Skip to content

feat: resolve MCP servers and skills from x-tfg-mcp and x-tfg-skills - #592

Open
pranjaltrvd wants to merge 2 commits into
mainfrom
feat/inline-mcp-and-skills
Open

feat: resolve MCP servers and skills from x-tfg-mcp and x-tfg-skills#592
pranjaltrvd wants to merge 2 commits into
mainfrom
feat/inline-mcp-and-skills

Conversation

@pranjaltrvd

@pranjaltrvd pranjaltrvd commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

In TrueFoundry mode, a request can now define the MCP servers and skills its agent spec names, rather than requiring them to be configured in the tenant's registry first.

Why

An embedded assistant ships its own agent definition. Registering its MCP servers in the tenant's registry to make that definition resolvable puts them in the tenant's settings and its gateway metrics, where they are noise at best and confusing at worst — they are not the tenant's servers and nobody there can meaningfully manage them.

Skills have the same problem with a sharper edge: PUT /settings/skills is admin-gated, so a least-privilege service identity cannot register one at all.

What

Two headers, each a JSON object keyed by resource name, mirroring x-tfg-mcp-headers:

x-tfg-mcp:    {"docs-mcp": {"url": "...", "description": "...", "auth": {"type": "header", "headers": {...}}}}
x-tfg-skills: {"ask-ai": {"url": "https://github.com/...", "ref": "<sha>", "path": "ask-ai", "description": "..."}}

type and name are implied and filled in during parsing, so what a caller sends is the part that is actually theirs to specify.

Definitions are request-scoped and never persisted. They take precedence over a registry entry of the same name for that request only.

How

Both resolve through the store seam that validateAgentSpec and the turn resolver already use, so no call site learns that the headers exist. resolveMcpServerStore(c) already took a Context; the skill store did not, so it becomes resolveSkillStore(c) to match the model and MCP resolvers. That rename is most of the diff.

Two behaviours worth calling out:

  • Credentials go to the upstream unchanged. An inline server's headers come from its own auth.headers, with no caller Bearer layered over them. That is what lets a rotating token ride each turn instead of being stored somewhere it goes stale six hours later.
  • An unfiltered list is untouched. Only by-name lookup and name-filtered list are overlaid, so request-scoped resources never surface in tenant settings or the chat picker.

dcr auth is rejected on an inline server: it needs a registered client and a stored token, neither of which a per-request definition has.

Testing

23 unit tests covering parsing, rejection cases, precedence, credential passthrough, and the unfiltered-list boundary. Full unit suite, both test tsconfigs and lint are clean.


Note

Medium Risk
Changes turn-time MCP/skill resolution and accepts client-supplied URLs and credentials in headers, though scoped to TrueFoundry mode with validation and no persistence.

Overview
In TrueFoundry mode, callers can supply MCP server and skill definitions per request via x-tfg-mcp and x-tfg-skills (JSON maps keyed by name). Those definitions are not persisted, override the tenant registry for that request only, and are used for agent spec validation and turn execution the same way as configured resources.

skillStore becomes resolveSkillStore(c) across routers and wiring so skills resolve per request like MCP and agents. When the headers are present, InlineMcpServerStore / InlineSkillStore decorate the inner stores: by-name lookup and name-filtered lists merge inline + registry; unfiltered lists stay registry-only so request-scoped resources do not appear in settings or pickers. Inline MCP auth.headers are sent upstream as-is (no caller Bearer layered on), supporting rotating tokens; DCR auth is rejected for inline MCP.

Parsing validates manifests (with implied type/name) and returns 400 on malformed headers rather than silent registry fallback. New unit tests cover parsing, store overlay behavior, and the Ask TFY wire contract (ASCII headers, size bounds).

Reviewed by Cursor Bugbot for commit 0608835. Bugbot is set up for automated code reviews on this repo. Configure here.

In TrueFoundry mode a request can define the MCP servers and skills its agent
spec names, instead of them being configured in the tenant's registry. The
definitions take precedence for that request only and are never persisted, so
they stay out of tenant settings and the gateway's metrics, and a rotating
credential can ride each turn rather than being stored somewhere it goes stale.

Both resolve through the store seam the spec validator and the turn resolver
already use, so no call site learns about the headers. Inline MCP credentials
come from the manifest's own auth.headers and reach the upstream unchanged --
no caller Bearer is layered over them. An unfiltered list still returns only
configured resources.

The skill store becomes request-scoped to carry this, mirroring the model and
MCP resolvers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0608835

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@truefoundry/trueforge Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0da6f6c. Configure here.

return undefined;
}
const now = new Date().toISOString();
return { id: name, tenant_id, name, manifest, created_at: now, updated_at: now };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline lookup hits inherited prototype keys

Low Severity

Bracket lookup on the plain #inline map treats inherited Object.prototype keys as request-scoped resources. constructor is a valid NameSchema name, so a by-name get or filtered list can return a fake record whose manifest is the Object function instead of falling through to the registry.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0da6f6c. Configure here.

raw: string,
header: string,
parseEntry: (name: string, definition: object) => EntryResult<TManifest>,
): Readonly<Record<string, TManifest>> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers take two positional strings

Low Severity

New helpers pass two string arguments positionally: parseByName takes raw and header, and both overlay #toRecord methods take tenant_id and name. Repo rules require a single options object when more than one parameter shares a type, so a swap is type-silent.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: TrueForge review rules

Reviewed by Cursor Bugbot for commit 0da6f6c. Configure here.

sfy-server builds the spec and the two headers in its own repo, so nothing
checked that the two agreed. This runs its exact output through AgentSpecSchema
and both inline parsers, and holds the header block to ASCII within one 8KB
ingress buffer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants