Skip to content

Support tool callbacks in MCP sampling#2998

Merged
Sayt-0 merged 7 commits into
docker:mainfrom
EronWright:sampling-tools
Jul 9, 2026
Merged

Support tool callbacks in MCP sampling#2998
Sayt-0 merged 7 commits into
docker:mainfrom
EronWright:sampling-tools

Conversation

@EronWright

@EronWright EronWright commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the tool callbacks functional gap in MCP sampling support — a follow-up to #2815, addressing one of the remaining items from #2809.

When an MCP server includes a tools array in a sampling/createMessage request, the host now drives its model with those tools and returns any tool_use blocks back to the server as ToolUseContent. The server remains responsible for executing the tool and continuing the loop in a follow-up sampling request.

sequenceDiagram
     participant H as cagent
      participant S as MCP Server
      participant L as LLM

      activate H
      H->>+S: tools/call {name, arguments}

      note over S: needs LLM inference

      S->>+H: sampling/createMessage<br/>{messages, tools: [...]}
      H->>+L: chat completion
      L-->>-H: ToolUseContent<br/>stopReason: "toolUse"
      H-->>-S: CreateMessageResult<br/>{tool_use, stopReason: "toolUse"}

      note over S: executes tool locally

      S->>+H: sampling/createMessage<br/>{messages + tool_use + tool_result, tools: [...]}
      H->>+L: chat completion
      L-->>-H: TextContent<br/>stopReason: "endTurn"
      H-->>-S: CreateMessageResult<br/>{text, stopReason: "endTurn"}

      S-->>-H: tool result
      deactivate H
Loading

What's new

  • New SamplingWithToolsHandler type and SampleableWithTools interface — additive, parallel to the existing SamplingHandler / Sampleable. No breaking changes to the basic sampling path merged in feat(mcp): add sampling/createMessage support #2815.
  • MCP toolset wires both handler types. At Initialize, exactly one of the SDK's mutually exclusive ClientOptions.CreateMessage* fields is populated — prefer with-tools when registered, fall back to basic.
  • Capability handshake advertises sampling.tools so servers know the host can receive tool-enabled requests.
  • Runtime handler (pkg/runtime/sampling.go):
    • Converts V2 multi-block messages: text, image/audio, tool_use → assistant ToolCalls, tool_resultMessageRoleTool rows (parallel tool_results expand to multiple chat.Message rows).
    • Converts []*mcp.Tool[]tools.Tool with a no-op handler (the server, not the host, executes).
    • Drives model.CreateChatCompletionStream, aggregates streamed tool calls.
    • Builds result Content with TextContent + ToolUseContent blocks; stopReason: "toolUse" when tool calls are present.
  • New limits: maxSamplingTools=64, maxSamplingToolCalls=32.
  • End-to-end test (e2e/sampling_test.go): mounts an in-process gomcp.NewServer on an httptest server via StreamableHTTPHandler. The server exposes one tool (ask_with_calculator) whose handler drives a real sampling-with-tools loop against the connecting cagent. The Gemini side is recorded once and replayed on subsequent runs, so the test runs offline in CI.

Out of scope (separate gaps from #2809)

  • Human-in-the-loop approval UI
  • Model-preference hints

Test plan

  • Unit tests pass: `go test ./pkg/runtime/... ./pkg/tools/...`
  • `go build ./...` clean
  • `go vet ./...` clean
  • `task lint` (0 offenses)
  • `gofmt` clean on all changed files
  • End-to-end via `TestExec_Gemini_SamplingWithTools` (cassette replays offline; verified once against live Gemini):
    • Handshake includes `sampling.tools` capability (implicit — `ServerSession.CreateMessageWithTools` would refuse otherwise)
    • LLM receives the server-supplied tools
    • Response contains `ToolUseContent` with `stopReason: "toolUse"` when the model emits a tool_use
    • Follow-up sampling request with `tool_result` blocks is converted correctly and the loop terminates with `endTurn`

@aheritier aheritier added area/agent For work that has to do with the general agent loop/agentic features of the app area/tools For features/issues/fixes related to the usage of built-in and MCP tools area/mcp MCP protocol, MCP tool servers, integration kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Jun 4, 2026
@EronWright EronWright marked this pull request as ready for review June 7, 2026 00:56
@EronWright EronWright requested a review from a team as a code owner June 7, 2026 00:56
@aheritier aheritier added the area/providers For features/issues/fixes related to LLM providers (Bedrock, LiteLLM, Qwen, custom, etc.) label Jun 7, 2026
@aheritier

Copy link
Copy Markdown
Contributor

/review

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

Two medium-severity findings in the newly-added sampling-with-tools code. The core stream-aggregation logic, capability handshake, content building, and limits enforcement are all well-structured and correctly tested.

Comment thread pkg/runtime/sampling.go Outdated
Comment thread pkg/tools/mcp/remote.go Outdated
@aheritier aheritier removed the area/providers For features/issues/fixes related to LLM providers (Bedrock, LiteLLM, Qwen, custom, etc.) label Jun 8, 2026
@EronWright EronWright marked this pull request as draft June 8, 2026 15:03
@EronWright EronWright requested a review from docker-agent June 8, 2026 20:35
@EronWright EronWright marked this pull request as ready for review June 8, 2026 20:38
@aheritier aheritier added area/testing Test infrastructure, CI/CD, test runners, evaluation area/providers/gemini Google Gemini provider support labels Jun 8, 2026
dgageot
dgageot previously approved these changes Jun 10, 2026
@dgageot

dgageot commented Jun 10, 2026

Copy link
Copy Markdown
Member

Sorry @EronWright, our project requires all the commit to be signed and verified. Could you sign them? Thanks!

@aheritier aheritier added area/testing Test infrastructure, CI/CD, test runners, evaluation status/blocked Author can't proceed and needs external help to get unblocked and removed area/testing Test infrastructure, CI/CD, test runners, evaluation area/providers/gemini Google Gemini provider support labels Jun 12, 2026
@aheritier aheritier marked this pull request as draft June 12, 2026 14:38
@aheritier

Copy link
Copy Markdown
Contributor

I moved it back to draft @EronWright the time you rewrite the history with signed commits. Sorry for the issue

@EronWright EronWright marked this pull request as ready for review June 15, 2026 15:06
@EronWright

Copy link
Copy Markdown
Contributor Author

@aheritier sorry for delay, all commits are now signed/verified.

@aheritier

Copy link
Copy Markdown
Contributor

/review

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

Two medium-severity issues found in the new sampling-with-tools path. Both relate to edge cases in the streaming and reconnect logic.

Comment thread pkg/runtime/sampling.go
Comment thread pkg/tools/mcp/stdio.go Outdated
@aheritier aheritier marked this pull request as draft June 22, 2026 13:04
@aheritier aheritier added the status/needs-rebase PR has merge conflicts or is out of date with main label Jun 22, 2026
@EronWright EronWright marked this pull request as ready for review June 23, 2026 06:36
@EronWright EronWright requested a review from dgageot June 23, 2026 16:59
@EronWright

Copy link
Copy Markdown
Contributor Author

The build issue seems for unrelated reasons, re-run?

@aheritier aheritier removed the status/needs-rebase PR has merge conflicts or is out of date with main label Jun 24, 2026
@aheritier

Copy link
Copy Markdown
Contributor

I think you rebased it on main which was red at that time. Can you rebase again @EronWright ?

@aheritier aheritier removed the status/blocked Author can't proceed and needs external help to get unblocked label Jun 26, 2026
Adds a parallel SamplingWithToolsHandler alongside the existing
SamplingHandler so MCP servers can include a tools array in
sampling/createMessage requests. The host drives its model with those
tools and returns any tool_use blocks as ToolUseContent; the server
remains responsible for executing the tool and continuing the loop in a
follow-up sampling request.

The initialize handshake now advertises sampling.tools capability, and
the MCP toolset selects the appropriate go-sdk handler (basic vs.
with-tools) based on which handler is registered.

Signed-off-by: Eron Wright <eronwright@gmail.com>
Mounts an in-process gomcp.NewServer on an httptest server via
StreamableHTTPHandler. Its one tool, ask_with_calculator, runs a
sampling loop: sends sampling/createMessage with a calculator tool,
gets a tool_use back from the host LLM, "executes" the calculator,
sends a follow-up sampling request carrying the tool_result, and
returns the final text. The Gemini side is recorded once and replayed
on subsequent runs, so the test runs offline in CI.

Signed-off-by: Eron Wright <eronwright@gmail.com>
- Reject tool_use blocks under a non-assistant role explicitly rather
  than silently dropping the message. The MCP spec places tool_use on
  assistant turns, but a malformed server would previously have its
  message disappear from the converted chat history with no error.
- Document the ordering dependency between SetSampling*Handler and
  Initialize in both stdio and remote MCP client setups, so future
  maintainers don't reorder them and silently lose the sampling
  handler on reconnect.

Signed-off-by: Eron Wright <eronwright@gmail.com>
Use errors.New for the static loop-exhausted message and strconv.FormatInt
for the calculator result instead of fmt.Errorf / fmt.Sprintf %d.

Signed-off-by: Eron Wright <eronwright@gmail.com>
- pkg/runtime/sampling.go: mirror the streaming.go short-circuit in
  drainSamplingStreamWithTools so a chunk that carries only tool-call
  deltas continues to the next frame instead of falling through into
  the FinishReason capture and break. The existing flow happens to be
  equivalent, but the explicit guard pre-empts regression if anyone
  inserts new processing between the tool-call loop and the break.

- pkg/tools/mcp/{stdio,remote,session_client}.go: collapse the inline
  CreateMessage* registration switch into a shared
  sessionClient.applySamplingHandlerOpts helper. The helper always
  wires the with-tools callback unless the caller has explicitly opted
  into the basic-only path, so a SetSamplingWithToolsHandler call
  landing after Initialize — including on supervisor-driven reconnect
  that races configureToolsetHandlers — still takes effect via the
  existing lazy read in handleSamplingWithToolsRequest. Closes the
  silent no-handler window the reviewer flagged on stdio.go:53 and
  remote.go:104.

Tests:
- TestDrainSamplingStreamWithTools/tool-call-only chunks continue ...
- TestApplySamplingHandlerOpts_RegistrationMatrix
- TestHandleSamplingWithToolsRequest_LateSetterTakesEffect

Signed-off-by: Eron Wright <eronwright@gmail.com>
@EronWright

Copy link
Copy Markdown
Contributor Author

Rebased on fbe505064 (current main; green). The previous build-and-test failure was on a red main revision as you suspected. One merge-fix folded into the base commit: main added a second tools.ConfigureHandlers call site in skillSubSessionTools (loop.go) that now also passes r.samplingWithToolsHandler. Local go build ./..., go test ./pkg/runtime/... ./pkg/tools/mcp/..., the sampling cassette e2e, and golangci-lint are all green. Ready for another CI approval.

@Sayt-0

Sayt-0 commented Jun 29, 2026

Copy link
Copy Markdown
Member

build-and-test fails: the e2e package panics with SetDefaultTokenStoreFactory called after the default token store was already created, because the new sampling test builds the global token store and the next Execute re-runs keyringstore.Register(). Making Register() idempotent (e.g. a sync.Once) should fix it.

@Sayt-0 Sayt-0 marked this pull request as draft July 1, 2026 08:25
root.Execute registers the keyring token store factory on every call.
Once a remote MCP toolset has materialized the process-wide store,
re-registering panicked with "SetDefaultTokenStoreFactory called after
the default token store was already created". The sampling e2e test is
the first in-process user of a remote MCP toolset, so any test calling
runCLI after it crashed the e2e package.

Guard Register with sync.Once: repeat registrations of the same factory
are no-ops, while a genuinely misordered first registration still panics.
@Sayt-0 Sayt-0 marked this pull request as ready for review July 8, 2026 12:07
@Sayt-0 Sayt-0 merged commit ae1a65e into docker:main Jul 9, 2026
8 checks passed
Piyush0049 pushed a commit to Piyush0049/docker-agent that referenced this pull request Jul 15, 2026
Support tool callbacks in MCP sampling
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent For work that has to do with the general agent loop/agentic features of the app area/mcp MCP protocol, MCP tool servers, integration area/testing Test infrastructure, CI/CD, test runners, evaluation area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants