Skip to content

feat(core): direct MCP tool calls, live registry sources, dynamic settings parity - #595

Merged
lIang70 merged 1 commit into
mainfrom
feat/core-mcp-direct-calls
Oct 8, 2026
Merged

lIang70 merged 1 commit into
mainfrom
feat/core-mcp-direct-calls

Conversation

@lIang70

@lIang70 lIang70 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Why

A host that wires MCP servers into its runtime can reach their tools only through the tool registry, which renames a tool (server__tool), scopes it to an agent and authorizes the call against that agent's grant set. That is the right contract for tools a model may pick from, and the wrong one for a graph node that already knows which tool it means — the shape the plugin-provided graph nodes consume.

Two smaller gaps come with it:

  • A registry created from a deployment's tool.registry settings stayed frozen at construction, so a source that publishes after the fact (a refresh, a background MCP connect) reached an assembly mount point but never the mounted registry.
  • An agent registered after the build (Runtime.RegisterAgent) or re-bound by Runtime.Reload expanded its settings with nil resolvers: a card or engine setting using a custom scheme or ${secret:...} worked in a deployment and failed the moment the same definition arrived at runtime.

What changed

  • core/tool/mcp — Source.CallTool(ctx, server, tool, args) calls one tool on a connected server directly, bypassing the registry. The result is raw JSON: the text content when the server sent any, the structured content when it sent no text mirror (SEP-2106 lets a server answer with data and no text, and reading "no text" as "no result" collapsed a real answer into {} without an error), {} when the result carries neither, and non-JSON text as a JSON string so the caller always holds valid JSON. An unknown server is NotFound, a server whose handshake has not finished is NotAvailable instead of a nil-session panic, and an errored result is an ordinary error carrying the server's own text or its structured content.
  • core/tool — RegistryFactory attaches every source that implements RegistryAttacher right after construction, so a registry built from settings stays live exactly like an assembly mount point.
  • core/deploy — Result.Resolver() and Result.Secrets() expose the effective settings reference resolver and secret resolver the build assembled (nil when the deployment declared none).
  • core/runtime — RegisterAgent and Reload pass those resolvers into deploy.BindAgent, so a dynamic or reloaded agent expands custom schemes and ${secret:...} exactly like a deployment-time one.
  • tests — TestDynamicAgentExpandsLikeDeployment (an engine factory captures the settings it was constructed with, on both the register and the reload re-bind path) and the CallTool suite (payload shapes, unknown server, unconnected server, error propagation).

Verification

  • make ci (vet + go test ./... across the workspace) passes.
  • make lint — 0 issues; gofmt -s -l core is empty; git diff --check clean.
  • make release-check — changesets valid; make release-plan reports core: 0.4.9 -> 0.4.10 (patch), tag core/v0.4.10.

Release notes

  • .release/core-v0.4.10.json covers the accumulated delta since core/v0.4.9: this PR plus the memory provenance change (Turn.MessageMetadata, SourceRef.ID) that shipped in feat(backends/memory): add the memory backend and its eval harness #583.
  • No coordinated release: core is the only release-managed module and nothing here changes a contract a driver/* module compiles against, so make release-preflight is not needed.

…tings parity

## What changed

- **core/tool/mcp**: `Source.CallTool` invokes one tool on a connected server
  directly, bypassing the registry that renames, re-scopes and re-authorizes a
  call. The result comes back as raw JSON — text content when the server sent
  any, structured content when it sent no text mirror, `{}` when it sent
  neither, non-JSON text as a JSON string. Unknown server is NotFound, a
  server without a finished handshake is NotAvailable, an errored result is an
  ordinary error carrying the server's own text.
- **core/tool**: `RegistryFactory` attaches every source implementing
  `RegistryAttacher` after construction, so a registry built from
  `tool.registry` settings stays live like an assembly mount point instead of
  missing publications that arrive after the fact.
- **core/deploy**: `Result.Resolver()` and `Result.Secrets()` expose the
  effective settings reference resolver and secret resolver the build
  assembled.
- **core/runtime**: `RegisterAgent` and `Reload` pass those resolvers into
  `deploy.BindAgent`; a dynamic or reloaded agent previously bound with
  `nil, nil`, so a setting using a custom scheme or `${secret:...}` failed
  when the same definition was registered at runtime.
- **tests**: `TestDynamicAgentExpandsLikeDeployment` (register + reload paths,
  via an engine factory that captures the settings it was constructed with)
  and the `CallTool` suite (payload shapes, unknown server, unconnected
  server, error propagation).

## Verification

- `make ci` (vet + test across the workspace) passes.
- `make lint` — 0 issues; `gofmt -s -l core` is empty.
- `make release-check` validates the changeset and the pending plan.
@lIang70
lIang70 merged commit 6326c74 into main Oct 8, 2026
20 checks passed
@lIang70
lIang70 deleted the feat/core-mcp-direct-calls branch October 8, 2026 15:06
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.

1 participant