Skip to content

Commit af06f96

Browse files
Copilotedburns
andauthored
Port preMcpToolCall hook support and bump maven-enforcer-plugin
- Add PreMcpToolCallHookInput, PreMcpToolCallHookOutput, PreMcpToolCallHandler - Add onPreMcpToolCall field to SessionHooks with hasHooks() check - Handle preMcpToolCall hook type in CopilotSession.handleHooksInvoke - Add E2E test for preMcpToolCall hook (set/replace/remove meta) - Add E2E test for MCPStdioServerConfig without args - Bump maven-enforcer-plugin from 3.6.2 to 3.6.3 Ports: f4d22d7, 38ca096, 23526eb from reference implementation Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent fcbb1ec commit af06f96

8 files changed

Lines changed: 564 additions & 3 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@
827827
<plugin>
828828
<groupId>org.apache.maven.plugins</groupId>
829829
<artifactId>maven-enforcer-plugin</artifactId>
830-
<version>3.6.2</version>
830+
<version>3.6.3</version>
831831
<executions>
832832
<execution>
833833
<id>require-schema-version</id>

src/main/java/com/github/copilot/sdk/CopilotSession.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import com.github.copilot.sdk.json.PermissionRequestResult;
8282
import com.github.copilot.sdk.json.PermissionRequestResultKind;
8383
import com.github.copilot.sdk.json.PostToolUseHookInput;
84+
import com.github.copilot.sdk.json.PreMcpToolCallHookInput;
8485
import com.github.copilot.sdk.json.PreToolUseHookInput;
8586
import com.github.copilot.sdk.json.SendMessageRequest;
8687
import com.github.copilot.sdk.json.SendMessageResponse;
@@ -1547,6 +1548,13 @@ CompletableFuture<Object> handleHooksInvoke(String hookType, JsonNode input) {
15471548
.thenApply(output -> (Object) output);
15481549
}
15491550
break;
1551+
case "preMcpToolCall" :
1552+
if (hooks.getOnPreMcpToolCall() != null) {
1553+
PreMcpToolCallHookInput mcpInput = MAPPER.treeToValue(input, PreMcpToolCallHookInput.class);
1554+
return hooks.getOnPreMcpToolCall().handle(mcpInput, invocation)
1555+
.thenApply(output -> (Object) output);
1556+
}
1557+
break;
15501558
case "postToolUse" :
15511559
if (hooks.getOnPostToolUse() != null) {
15521560
PostToolUseHookInput postInput = MAPPER.treeToValue(input, PostToolUseHookInput.class);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
import java.util.concurrent.CompletableFuture;
8+
9+
/**
10+
* Handler for pre-MCP-tool-call hooks.
11+
* <p>
12+
* This hook is called before an MCP tool call is dispatched to an MCP server,
13+
* allowing you to:
14+
* <ul>
15+
* <li>Inspect the tool call arguments and server name</li>
16+
* <li>Set, replace, or remove MCP request metadata ({@code _meta})</li>
17+
* </ul>
18+
*
19+
* @since 1.0.8
20+
*/
21+
@FunctionalInterface
22+
public interface PreMcpToolCallHandler {
23+
24+
/**
25+
* Handles a pre-MCP-tool-call hook invocation.
26+
*
27+
* @param input
28+
* the hook input containing server name, tool name, and arguments
29+
* @param invocation
30+
* context information about the invocation
31+
* @return a future that resolves with the hook output, or {@code null} to
32+
* preserve existing metadata (no-op)
33+
*/
34+
CompletableFuture<PreMcpToolCallHookOutput> handle(PreMcpToolCallHookInput input, HookInvocation invocation);
35+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
import java.util.Map;
8+
9+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
10+
import com.fasterxml.jackson.annotation.JsonProperty;
11+
import com.fasterxml.jackson.databind.JsonNode;
12+
13+
/**
14+
* Input for a pre-MCP-tool-call hook.
15+
* <p>
16+
* This hook fires before an MCP tool call is dispatched to an MCP server,
17+
* allowing you to inspect or modify the request metadata.
18+
*
19+
* @since 1.0.8
20+
*/
21+
@JsonIgnoreProperties(ignoreUnknown = true)
22+
public class PreMcpToolCallHookInput {
23+
24+
@JsonProperty("sessionId")
25+
private String sessionId;
26+
27+
@JsonProperty("timestamp")
28+
private long timestamp;
29+
30+
@JsonProperty("cwd")
31+
private String cwd;
32+
33+
@JsonProperty("serverName")
34+
private String serverName;
35+
36+
@JsonProperty("toolName")
37+
private String toolName;
38+
39+
@JsonProperty("arguments")
40+
private JsonNode arguments;
41+
42+
@JsonProperty("toolCallId")
43+
private String toolCallId;
44+
45+
@JsonProperty("_meta")
46+
private Map<String, JsonNode> meta;
47+
48+
/**
49+
* Gets the runtime session ID of the session that triggered the hook.
50+
*
51+
* @return the session ID
52+
*/
53+
public String getSessionId() {
54+
return sessionId;
55+
}
56+
57+
/**
58+
* Sets the runtime session ID of the session that triggered the hook.
59+
*
60+
* @param sessionId
61+
* the session ID
62+
* @return this instance for method chaining
63+
*/
64+
public PreMcpToolCallHookInput setSessionId(String sessionId) {
65+
this.sessionId = sessionId;
66+
return this;
67+
}
68+
69+
/**
70+
* Gets the timestamp of the hook invocation.
71+
*
72+
* @return the timestamp in milliseconds
73+
*/
74+
public long getTimestamp() {
75+
return timestamp;
76+
}
77+
78+
/**
79+
* Sets the timestamp of the hook invocation.
80+
*
81+
* @param timestamp
82+
* the timestamp in milliseconds
83+
* @return this instance for method chaining
84+
*/
85+
public PreMcpToolCallHookInput setTimestamp(long timestamp) {
86+
this.timestamp = timestamp;
87+
return this;
88+
}
89+
90+
/**
91+
* Gets the current working directory.
92+
*
93+
* @return the working directory path
94+
*/
95+
public String getCwd() {
96+
return cwd;
97+
}
98+
99+
/**
100+
* Sets the current working directory.
101+
*
102+
* @param cwd
103+
* the working directory path
104+
* @return this instance for method chaining
105+
*/
106+
public PreMcpToolCallHookInput setCwd(String cwd) {
107+
this.cwd = cwd;
108+
return this;
109+
}
110+
111+
/**
112+
* Gets the name of the MCP server being called.
113+
*
114+
* @return the server name
115+
*/
116+
public String getServerName() {
117+
return serverName;
118+
}
119+
120+
/**
121+
* Sets the name of the MCP server being called.
122+
*
123+
* @param serverName
124+
* the server name
125+
* @return this instance for method chaining
126+
*/
127+
public PreMcpToolCallHookInput setServerName(String serverName) {
128+
this.serverName = serverName;
129+
return this;
130+
}
131+
132+
/**
133+
* Gets the name of the MCP tool being called.
134+
*
135+
* @return the tool name
136+
*/
137+
public String getToolName() {
138+
return toolName;
139+
}
140+
141+
/**
142+
* Sets the name of the MCP tool being called.
143+
*
144+
* @param toolName
145+
* the tool name
146+
* @return this instance for method chaining
147+
*/
148+
public PreMcpToolCallHookInput setToolName(String toolName) {
149+
this.toolName = toolName;
150+
return this;
151+
}
152+
153+
/**
154+
* Gets the arguments for the MCP tool call.
155+
*
156+
* @return the arguments as a JSON node, or {@code null}
157+
*/
158+
public JsonNode getArguments() {
159+
return arguments;
160+
}
161+
162+
/**
163+
* Sets the arguments for the MCP tool call.
164+
*
165+
* @param arguments
166+
* the arguments as a JSON node
167+
* @return this instance for method chaining
168+
*/
169+
public PreMcpToolCallHookInput setArguments(JsonNode arguments) {
170+
this.arguments = arguments;
171+
return this;
172+
}
173+
174+
/**
175+
* Gets the tool call ID, if available.
176+
*
177+
* @return the tool call ID, or {@code null}
178+
*/
179+
public String getToolCallId() {
180+
return toolCallId;
181+
}
182+
183+
/**
184+
* Sets the tool call ID.
185+
*
186+
* @param toolCallId
187+
* the tool call ID
188+
* @return this instance for method chaining
189+
*/
190+
public PreMcpToolCallHookInput setToolCallId(String toolCallId) {
191+
this.toolCallId = toolCallId;
192+
return this;
193+
}
194+
195+
/**
196+
* Gets the MCP request metadata, if present.
197+
*
198+
* @return the metadata map, or {@code null}
199+
*/
200+
public Map<String, JsonNode> getMeta() {
201+
return meta;
202+
}
203+
204+
/**
205+
* Sets the MCP request metadata.
206+
*
207+
* @param meta
208+
* the metadata map
209+
* @return this instance for method chaining
210+
*/
211+
public PreMcpToolCallHookInput setMeta(Map<String, JsonNode> meta) {
212+
this.meta = meta;
213+
return this;
214+
}
215+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
import com.fasterxml.jackson.annotation.JsonInclude;
8+
import com.fasterxml.jackson.annotation.JsonProperty;
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
11+
/**
12+
* Output for a pre-MCP-tool-call hook.
13+
* <p>
14+
* The {@link #metaToUse} property controls outgoing MCP request metadata:
15+
* <ul>
16+
* <li>Return {@code null} from the hook handler: preserve existing
17+
* {@code _meta} (no-op).</li>
18+
* <li>Return a {@code PreMcpToolCallHookOutput} with {@code metaToUse} left as
19+
* {@code null}: remove {@code _meta} from the request.</li>
20+
* <li>Return a {@code PreMcpToolCallHookOutput} with {@code metaToUse} set to a
21+
* JSON object: replace {@code _meta} with that object.</li>
22+
* </ul>
23+
*
24+
* @since 1.0.8
25+
*/
26+
@JsonInclude(JsonInclude.Include.ALWAYS)
27+
public class PreMcpToolCallHookOutput {
28+
29+
@JsonProperty("metaToUse")
30+
private JsonNode metaToUse;
31+
32+
/**
33+
* Gets the metadata to use for the outgoing MCP request.
34+
*
35+
* @return the metadata JSON node, or {@code null} to remove metadata
36+
*/
37+
public JsonNode getMetaToUse() {
38+
return metaToUse;
39+
}
40+
41+
/**
42+
* Sets the metadata to use for the outgoing MCP request.
43+
*
44+
* @param metaToUse
45+
* the metadata JSON node, or {@code null} to remove metadata
46+
* @return this instance for method chaining
47+
*/
48+
public PreMcpToolCallHookOutput setMetaToUse(JsonNode metaToUse) {
49+
this.metaToUse = metaToUse;
50+
return this;
51+
}
52+
53+
/**
54+
* Creates a hook output that sets the given metadata on the MCP request.
55+
*
56+
* @param metaToUse
57+
* the metadata JSON node to use
58+
* @return the hook output
59+
*/
60+
public static PreMcpToolCallHookOutput withMeta(JsonNode metaToUse) {
61+
return new PreMcpToolCallHookOutput().setMetaToUse(metaToUse);
62+
}
63+
64+
/**
65+
* Creates a hook output that removes metadata from the MCP request.
66+
*
67+
* @return the hook output with {@code null} metaToUse
68+
*/
69+
public static PreMcpToolCallHookOutput removeMeta() {
70+
return new PreMcpToolCallHookOutput();
71+
}
72+
}

0 commit comments

Comments
 (0)