Add preMcpToolCall hook support to all SDKs#1366
Conversation
There was a problem hiding this comment.
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
preMcpToolCallhook handlers and input/output types across Rust, .NET, Go, Node.js, and Python (including runtime dispatch/handler registration). - Renames existing hook input
cwdfields toWorkingDirectory/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
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>
532d038 to
895ff7d
Compare
This comment has been minimized.
This comment has been minimized.
895ff7d to
c3e0240
Compare
This comment has been minimized.
This comment has been minimized.
c3e0240 to
88c2714
Compare
This comment has been minimized.
This comment has been minimized.
88c2714 to
929528b
Compare
This comment has been minimized.
This comment has been minimized.
929528b to
2020647
Compare
This comment has been minimized.
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>
2020647 to
ff1c65d
Compare
This comment has been minimized.
This comment has been minimized.
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.
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>
|
Commit pushed:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1366 · ● 13.3M
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>
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>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
- 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>
This comment has been minimized.
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>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency Review ✅This PR maintains excellent cross-SDK consistency. Here's a summary of the review: CoverageThe API naming consistency (language idioms respected)
|
Motivation
The copilot-agent-runtime 1.0.51 added a new
preMcpToolCallhook that lets SDK consumers intercept MCP tool invocations before they execute -- specifically to inspect, set, replace, or remove the_metafield 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:_metaunchangedmetaToUseset to an object -- replace_metawith that objectmetaToUseset to null/None -- remove_metaentirelyKey design decisions
"cwd"but the public SDK API exposes it asWorkingDirectory/working_directoryfor consistency with the rest of the SDK surface. All existing hook input types were renamed fromCwdtoWorkingDirectoryas well (with serialization attributes preserving the wire name).SerializeHookOutputhelper to pre-serialize the typed output toJsonElementbefore boxing into the genericHooksInvokeResponse(object? Output)-- required because reflection-based serialization is disabled in the project.hasHookschecks in .NET and Go were updated to includeOnPreMcpToolCallso the runtime knows to invoke hooks.Testing
test/harness/test-mcp-meta-echo-server.mjs) that echoes back the_metait received in the tool resulttest/snapshots/pre_mcp_tool_call_hook/Files of note
test/harness/test-mcp-meta-echo-server.mjs-- shared MCP server for E2E testsdotnet/src/Types.cs--PreMcpToolCallHookInputandPreMcpToolCallHookOutputtypesrust/src/hooks.rs-- Rust hook trait extension and dispatch logictest/harness/replayingCapiProxy.ts-- minor fix to forward hook RPC calls correctly