Skip to content

Add preMcpToolCall hook support to all SDKs#1366

Open
stephentoub wants to merge 15 commits into
mainfrom
stephentoub/pre-mcp-tool-use-hook
Open

Add preMcpToolCall hook support to all SDKs#1366
stephentoub wants to merge 15 commits into
mainfrom
stephentoub/pre-mcp-tool-use-hook

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

Motivation

The copilot-agent-runtime 1.0.51 added a new preMcpToolCall hook that lets SDK consumers intercept MCP tool invocations before they execute -- specifically to inspect, set, replace, or remove the _meta field sent to the MCP server. This PR adds support for that hook across all five language SDKs.

Approach

Each SDK receives a new hook callback (OnPreMcpToolCall / on_pre_mcp_tool_call) that is invoked with contextual information about the pending MCP tool call (server name, tool name, arguments, working directory, timestamp, existing meta). The hook returns a tri-state output:

  • Return null/None -- preserve the existing _meta unchanged
  • Return output with metaToUse set to an object -- replace _meta with that object
  • Return output with metaToUse set to null/None -- remove _meta entirely

Key design decisions

  • Wire format uses "cwd" but the public SDK API exposes it as WorkingDirectory/working_directory for consistency with the rest of the SDK surface. All existing hook input types were renamed from Cwd to WorkingDirectory as well (with serialization attributes preserving the wire name).
  • .NET uses a SerializeHookOutput helper to pre-serialize the typed output to JsonElement before boxing into the generic HooksInvokeResponse(object? Output) -- required because reflection-based serialization is disabled in the project.
  • The hasHooks checks in .NET and Go were updated to include OnPreMcpToolCall so the runtime knows to invoke hooks.

Testing

  • E2E tests added for all 5 SDKs (15 new tests total), covering all three meta manipulation scenarios (set, replace, remove)
  • Tests use a shared MCP "meta-echo" server (test/harness/test-mcp-meta-echo-server.mjs) that echoes back the _meta it received in the tool result
  • Three new snapshot YAML files in test/snapshots/pre_mcp_tool_call_hook/
  • .NET serialization unit tests verifying correct JSON output for all hook return shapes
  • All tests pass locally across .NET, Node.js, Go, Python, and Rust

Files of note

  • test/harness/test-mcp-meta-echo-server.mjs -- shared MCP server for E2E tests
  • dotnet/src/Types.cs -- PreMcpToolCallHookInput and PreMcpToolCallHookOutput types
  • rust/src/hooks.rs -- Rust hook trait extension and dispatch logic
  • test/harness/replayingCapiProxy.ts -- minor fix to forward hook RPC calls correctly

Copilot AI review requested due to automatic review settings May 21, 2026 17:42
@stephentoub stephentoub requested a review from a team as a code owner May 21, 2026 17:42
Comment thread dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs
Comment thread dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds cross-SDK support for the new preMcpToolCall runtime hook so consumers can inspect/modify/remove MCP request _meta before tool dispatch, and updates hook payload APIs to expose WorkingDirectory/workingDirectory while preserving the on-the-wire "cwd" key.

Changes:

  • Introduces preMcpToolCall hook handlers and input/output types across Rust, .NET, Go, Node.js, and Python (including runtime dispatch/handler registration).
  • Renames existing hook input cwd fields to WorkingDirectory/workingDirectory (with wire-format mapping back to "cwd").
  • Adds shared MCP “meta-echo” test server + snapshot-driven E2E tests for set/replace/remove-meta scenarios; adds .NET serialization tests for hook output shapes.
Show a summary per file
File Description
test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml New replay snapshot for “set meta” scenario.
test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml New replay snapshot for “replace meta” scenario.
test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml New replay snapshot for “remove meta” scenario.
test/harness/test-mcp-meta-echo-server.mjs Adds MCP stdio server that echoes received _meta for E2E validation.
test/harness/replayingCapiProxy.ts Adds optional debug logging to help diagnose snapshot prefix mismatches.
rust/tests/e2e/support.rs Removes now-unneeded dead_code suppression for repo_root().
rust/tests/e2e/pre_mcp_tool_call_hook.rs Adds Rust E2E tests covering set/replace/remove meta via hook.
rust/tests/e2e/hooks_extended.rs Updates tests to assert working_directory instead of cwd.
rust/tests/e2e.rs Registers new Rust E2E module.
rust/src/hooks.rs Adds PreMcpToolCall hook types + dispatch; renames hook cwd fields to working_directory (wire "cwd").
rust/examples/hooks.rs Updates example to use working_directory.
python/e2e/test_pre_mcp_tool_call_hook_e2e.py Adds Python E2E tests for preMcpToolCall meta manipulation.
python/e2e/test_hooks_extended_e2e.py Updates extended hook test to expect workingDirectory.
python/copilot/session.py Adds new hook TypedDicts/handler type and dispatch wiring; remaps "cwd" to workingDirectory for hook inputs.
nodejs/test/e2e/pre_mcp_tool_call_hook.e2e.test.ts Adds Node E2E tests for preMcpToolCall meta manipulation.
nodejs/test/e2e/hooks_extended.e2e.test.ts Updates extended hook test assertions to use workingDirectory.
nodejs/src/types.ts Adds new PreMcpToolCall* types and hook registration; renames hook base input cwd to workingDirectory.
nodejs/src/session.ts Remaps raw hook input { cwd } to { workingDirectory } during deserialization.
go/types.go Adds Go PreMcpToolCall hook types + meta tri-state helpers; renames several public Cwd-named fields to WorkingDirectory (wire "cwd").
go/session.go Wires preMcpToolCall into hook invoke dispatcher.
go/internal/e2e/session_e2e_test.go Updates assertions to use Context.WorkingDirectory.
go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go Adds Go E2E tests for meta set/replace/remove via hook.
go/internal/e2e/mcp_and_agents_e2e_test.go Updates MCP stdio config field to WorkingDirectory.
go/internal/e2e/hooks_extended_e2e_test.go Updates hook extended test to use WorkingDirectory.
go/internal/e2e/client_options_e2e_test.go Updates captured CLI JSON struct to WorkingDirectory field (wire "cwd").
go/client.go Updates hasHooks detection to include OnPreMcpToolCall.
dotnet/test/Unit/SerializationTests.cs Adds unit tests validating serialization shapes for PreMcpToolCallHookOutput and boxed JsonElement outputs.
dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs Adds .NET E2E tests for meta set/replace/remove via hook.
dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs Updates hook lifecycle assertions to use WorkingDirectory.
dotnet/src/Types.cs Adds PreMcpToolCallHookInput/Output; renames hook input Cwd properties to WorkingDirectory (wire "cwd").
dotnet/src/Session.cs Adds hook dispatch for preMcpToolCall and pre-serialization helper for output.
dotnet/src/Client.cs Updates hasHooks detection to include OnPreMcpToolCall.

Copilot's findings

  • Files reviewed: 32/32 changed files
  • Comments generated: 4

Comment thread nodejs/src/session.ts
Comment thread python/copilot/session.py
Comment thread go/types.go
Comment thread go/types.go Outdated
stephentoub and others added 2 commits May 21, 2026 13:50
Add the preMcpToolCall hook which fires before an MCP tool call is
dispatched to an MCP server. This aligns with copilot-agent-runtime
1.0.51 which added support for this hook type.

The hook receives serverName, toolName, arguments, optional toolCallId,
and optional _meta as input. The output supports a tri-state metaToUse
field: absent (preserve existing _meta), null (remove _meta), or object
(replace _meta).

Changes per SDK:
- Node.js: PreMcpToolCallHookInput/Output types, handler, SessionHooks
- Python: PreMcpToolCallHookInput/Output TypedDicts, handler, SessionHooks
- Go: PreMcpToolCallHookInput/Output structs, handler, helper functions
- .NET: PreMcpToolCallHookInput/Output classes, SessionHooks, JsonElement?
- Rust: PreMcpToolCallInput/Output structs, HookEvent/Output variants, trait

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the three preMcpToolCall hook test scenarios (set meta, replace meta,
remove meta) from the .NET reference implementation to all four remaining
SDK test suites.

Each test:
- Configures an MCP stdio server (meta-echo) that echoes _meta back
- Registers a preMcpToolCall hook that sets/replaces/removes metadata
- Verifies the tool result reflects the hook's effect
- Asserts hook input fields (serverName, toolName, workingDirectory, timestamp)

Snapshot files are reused from test/snapshots/pre_mcp_tool_call_hook/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from 532d038 to 895ff7d Compare May 21, 2026 17:51
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from 895ff7d to c3e0240 Compare May 21, 2026 17:57
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from c3e0240 to 88c2714 Compare May 21, 2026 18:06
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from 88c2714 to 929528b Compare May 21, 2026 18:25
@github-actions

This comment has been minimized.

@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from 929528b to 2020647 Compare May 21, 2026 18:30
@github-actions

This comment has been minimized.

- Add OnPreMcpToolCall to hasHooks checks in .NET Client.cs and Go client.go
- Add SerializeHookOutput helper for source-gen serialization
- Add .NET PreMcpToolCallHookE2ETests (3 tests: set, replace, remove meta)
- Add MCP meta-echo test server and snapshot YAML files
- Fix Go mcp_and_agents_e2e_test.go (Cwd -> WorkingDirectory)
- Remove stale dead_code lint expectation in Rust support.rs
- Add serialization unit tests for hook output

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub force-pushed the stephentoub/pre-mcp-tool-use-hook branch from 2020647 to ff1c65d Compare May 21, 2026 18:48
@github-actions

This comment has been minimized.

stephentoub and others added 2 commits May 21, 2026 15:43
The Copilot CLI now returns ISO-8601 timestamp strings instead of numeric
epoch milliseconds. Update PingResponse.timestamp from long to String and
PingResult.timestamp from Long to String. Update corresponding test
assertions accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Auto-committed by java-codegen-check workflow.
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label May 21, 2026
The generated PingResult record has timestamp as Long (milliseconds),
but tests were passing String values (ISO date format). Update tests
to use Long millisecond values instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: bd6ef76

Generated by Java Codegen Agentic Fix · ● 9.2M

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1366 · ● 13.3M

Comment thread go/types.go
stephentoub and others added 2 commits May 21, 2026 16:25
Rename the public `cwd` field to `working_directory` on:
- ClientOptions (local-only, not serialized)
- McpStdioServerConfig (serialized; add #[serde(rename = "cwd")])
- SessionListFilter (serialized; add #[serde(rename = "cwd")])

The wire format remains unchanged (JSON key stays "cwd").

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename all public API fields named 'cwd' to 'working_directory' and
'initial_cwd' to 'initial_working_directory' in the Python SDK while
preserving the wire format (JSON sent to/from the Copilot CLI runtime
still uses 'cwd' and 'initialCwd').

Fields renamed:
- SubprocessConfig.cwd -> working_directory
- MCPStdioServerConfig['cwd'] -> working_directory
- SessionContext.cwd -> working_directory
- SessionListFilter.cwd -> working_directory
- SessionFsConfig['initial_cwd'] -> initial_working_directory

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
stephentoub and others added 2 commits May 21, 2026 16:25
Rename the public-facing 'cwd' field to 'workingDirectory' in:
- CopilotClientOptions.cwd
- MCPStdioServerConfig.cwd
- SessionContext.cwd
- SessionListFilter.cwd

The wire format (JSON sent to/from the Copilot CLI runtime) is preserved
as 'cwd' via transformation layers in client.ts:
- Outgoing: workingDirectory -> cwd (mcpServers, customAgents, listSessions filter)
- Incoming: cwd -> workingDirectory (session context in toSessionMetadata)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

stephentoub and others added 2 commits May 21, 2026 17:39
- Node.js: Increase CLI server start timeout from 10s to 30s to accommodate
  slow Windows CI runners
- Java: Add missing conversation to mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents
  snapshot (was empty, causing proxy 500 errors)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…Go and .NET

Complete the cwd → workingDirectory rename across all SDKs for consistency.
Wire format (JSON) is preserved via struct tags and JsonPropertyName attributes.

Go:
- ClientOptions.Cwd → WorkingDirectory
- SessionFsConfig.InitialCwd → InitialWorkingDirectory
- SessionContext.Cwd → WorkingDirectory (json:"cwd")
- SessionListFilter.Cwd → WorkingDirectory (json:"cwd,omitempty")

.NET:
- SessionFsConfig.InitialCwd → InitialWorkingDirectory ([JsonPropertyName("initialCwd")])
- SessionContext.Cwd → WorkingDirectory ([JsonPropertyName("cwd")])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR maintains excellent cross-SDK consistency. Here's a summary of the review:

Coverage

The preMcpToolCall hook is added to all 6 SDK implementations: Node.js/TypeScript, Python, Go, .NET, Java, and Rust.

API naming consistency (language idioms respected)

SDK Hook property Input type Output type
.NET OnPreMcpToolCall PreMcpToolCallHookInput PreMcpToolCallHookOutput?
Go OnPreMcpToolCall PreMcpToolCallHookInput *PreMcpToolCallHookOutput
Java setOnPreMcpToolCall / getOnPreMcpToolCall PreMcpToolCallHookInput PreMcpToolCallHookOutput
Node.js onPreMcpToolCall PreMcpToolCallHookInput PreMcpToolCallHookOutput | null | undefined
Python on_pre_mcp_tool_call PreMcpToolCallHookInput PreMcpToolCallHookOutput | None
Rust (trait-based) PreMcpToolCallHookInput PreMcpToolCallHookOutput

cwdWorkingDirectory rename

The wire-format rename is applied consistently across all SDKs — public API uses WorkingDirectory/working_directory while JSON serialization preserves the "cwd" wire name via attributes/tags.

Test coverage

E2E tests for all three meta-manipulation scenarios (set, replace, remove) are present for all SDKs.

No inconsistencies found.

Generated by SDK Consistency Review Agent for issue #1366 · ● 7.2M ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants