Skip to content

Commit b8e9eb9

Browse files
authored
Merge pull request #172 from WolframResearch/feature/mcp-roots
Add MCP roots support for working-directory propagation
2 parents d7bbcac + 19c430d commit b8e9eb9

20 files changed

Lines changed: 1737 additions & 64 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ See [building.md](docs/building.md) for detailed instructions.
4848
- `Files.wl`: Helper functions for file operations
4949
- `Formatting.wl`: Definitions for formatting in notebooks
5050
- `InstallMCPServer.wl`: Implementation for installing MCP servers for use in some common MCP client applications
51+
- `MCPClientRequests.wl`: Server-to-client request infrastructure (request registry, response correlation, notification dispatch) used by [MCP roots](docs/mcp-roots.md) and other server-initiated requests
52+
- `MCPRoots.wl`: [MCP roots](docs/mcp-roots.md) handshake — issues `roots/list`, normalizes `file://` URIs (including malformed Windows variants), and applies the selected directory to the kernel, evaluator, and `RunProcess` calls
5153
- `MCPServerObject.wl`: Defines the MCP server object format
5254
- `Messages.wl`: Definitions for error messages
5355
- `PacletExtension.wl`: Paclet discovery, name resolution, and definition loading for the [paclet extension](docs/paclet-extensions.md) system
@@ -89,6 +91,7 @@ See [building.md](docs/building.md) for detailed instructions.
8991
- `code-inspector-rules.md`: Adding custom CodeInspector rules
9092
- `agent-skills.md`: Agent skills system, build process, and how to add new skills
9193
- `deploy-agent-tools.md`: Deployment management for agent tools
94+
- `mcp-roots.md`: MCP roots handshake, working-directory propagation, and guidance for tools that resolve relative paths
9295
- `paclet-extensions.md`: Third-party paclet extension system for contributing tools, prompts, and servers
9396

9497
### MCP Documentation

Kernel/CommonSymbols.wl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ BeginPackage[ "Wolfram`AgentTools`Common`" ];
106106
(* Logging utilities: *)
107107
`debugPrint;
108108
`writeError;
109+
`writeLog;
110+
111+
(* MCP client requests / server-to-client traffic: *)
112+
`$mcpClientRequests;
113+
`handleClientResponse;
114+
`handleNotification;
115+
`onClientInitialized;
116+
`onRootsListChanged;
117+
`sendClientRequest;
118+
119+
(* MCP roots: *)
120+
`$clientSupportsRoots;
121+
`$mcpRoot;
122+
`useEvaluatorKernel;
109123

110124
(* MCP Apps / UI resources: *)
111125
`$clientSupportsUI;

Kernel/MCPClientRequests.wl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
(* ::Section::Closed:: *)
2+
(*Package Header*)
3+
BeginPackage[ "Wolfram`AgentTools`MCPClientRequests`" ];
4+
Begin[ "`Private`" ];
5+
6+
Needs[ "Wolfram`AgentTools`" ];
7+
Needs[ "Wolfram`AgentTools`Common`" ];
8+
9+
(* ::**************************************************************************************************************:: *)
10+
(* ::Section::Closed:: *)
11+
(*Pending-Request Registry*)
12+
$mcpClientRequests = <| |>;
13+
14+
(* ::**************************************************************************************************************:: *)
15+
(* ::Section::Closed:: *)
16+
(*sendClientRequest*)
17+
sendClientRequest // beginDefinition;
18+
19+
sendClientRequest[ method_String, params_, handler_ ] :=
20+
Module[ { uuid, request },
21+
uuid = CreateUUID[ ];
22+
request = <|
23+
"jsonrpc" -> "2.0",
24+
"id" -> uuid,
25+
"method" -> method,
26+
"params" -> params
27+
|>;
28+
$mcpClientRequests[ uuid ] = <|
29+
"id" -> uuid,
30+
"request" -> request,
31+
"handler" -> handler
32+
|>;
33+
WriteLine[ "stdout", Developer`WriteRawJSONString[ request, "Compact" -> True ] ];
34+
writeLog[ "ClientRequest" -> request ];
35+
uuid
36+
];
37+
38+
sendClientRequest // endDefinition;
39+
40+
(* ::**************************************************************************************************************:: *)
41+
(* ::Section::Closed:: *)
42+
(*handleClientResponse*)
43+
handleClientResponse // beginDefinition;
44+
45+
handleClientResponse[ id_String, message_Association ] :=
46+
Catch @ Module[ { entry, handler, request },
47+
entry = Lookup[ $mcpClientRequests, id, None ];
48+
If[ entry === None, Throw @ Null ];
49+
handler = entry[ "handler" ];
50+
request = entry[ "request" ];
51+
KeyDropFrom[ $mcpClientRequests, id ];
52+
handler[ request, message ]
53+
];
54+
55+
handleClientResponse // endDefinition;
56+
57+
(* ::**************************************************************************************************************:: *)
58+
(* ::Section::Closed:: *)
59+
(*handleNotification*)
60+
handleNotification // beginDefinition;
61+
62+
handleNotification[ "notifications/initialized" , msg_ ] := onClientInitialized @ msg;
63+
handleNotification[ "notifications/roots/list_changed" , msg_ ] := onRootsListChanged @ msg;
64+
handleNotification[ _, _ ] := Null;
65+
66+
handleNotification // endDefinition;
67+
68+
(* ::**************************************************************************************************************:: *)
69+
(* ::Section::Closed:: *)
70+
(*Package Footer*)
71+
addToMXInitialization[
72+
$mcpClientRequests = <| |>;
73+
];
74+
75+
End[ ];
76+
EndPackage[ ];

Kernel/MCPRoots.wl

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(* ::Section::Closed:: *)
2+
(*Package Header*)
3+
BeginPackage[ "Wolfram`AgentTools`MCPRoots`" ];
4+
Begin[ "`Private`" ];
5+
6+
Needs[ "Wolfram`AgentTools`" ];
7+
Needs[ "Wolfram`AgentTools`Common`" ];
8+
9+
(* ::**************************************************************************************************************:: *)
10+
(* ::Section::Closed:: *)
11+
(*Session State*)
12+
$clientSupportsRoots = False;
13+
$mcpRoot = None;
14+
15+
(* ::**************************************************************************************************************:: *)
16+
(* ::Section::Closed:: *)
17+
(*onClientInitialized*)
18+
onClientInitialized // beginDefinition;
19+
20+
onClientInitialized[ _ ] :=
21+
If[ TrueQ @ $clientSupportsRoots,
22+
sendClientRequest[ "roots/list", <| |>, handleRootsListResponse ]
23+
];
24+
25+
onClientInitialized // endDefinition;
26+
27+
(* ::**************************************************************************************************************:: *)
28+
(* ::Section::Closed:: *)
29+
(*onRootsListChanged*)
30+
onRootsListChanged // beginDefinition;
31+
32+
onRootsListChanged[ _ ] :=
33+
If[ TrueQ @ $clientSupportsRoots,
34+
sendClientRequest[ "roots/list", <| |>, handleRootsListResponse ]
35+
];
36+
37+
onRootsListChanged // endDefinition;
38+
39+
(* ::**************************************************************************************************************:: *)
40+
(* ::Section::Closed:: *)
41+
(*handleRootsListResponse*)
42+
handleRootsListResponse // beginDefinition;
43+
44+
handleRootsListResponse[ request_, response_Association ] :=
45+
Catch @ Module[ { roots, root },
46+
If[ KeyExistsQ[ response, "error" ],
47+
writeLog[ "RootsListError" -> response[ "error" ] ];
48+
Throw @ Null
49+
];
50+
roots = response[ "result", "roots" ];
51+
root = pickFirstValidRoot @ roots;
52+
If[ StringQ @ root,
53+
applyMCPRoot @ root,
54+
writeLog[ "RootsListEmptyOrInvalid" -> roots ]
55+
]
56+
];
57+
58+
handleRootsListResponse // endDefinition;
59+
60+
(* ::**************************************************************************************************************:: *)
61+
(* ::Section::Closed:: *)
62+
(*pickFirstValidRoot*)
63+
pickFirstValidRoot // beginDefinition;
64+
65+
pickFirstValidRoot[ roots_List ] :=
66+
SelectFirst[
67+
rootURIToPath /@ Cases[ roots, KeyValuePattern[ "uri" -> uri_String ] :> uri ],
68+
StringQ[ # ] && DirectoryQ[ # ] &,
69+
None
70+
];
71+
72+
pickFirstValidRoot[ _ ] := None;
73+
74+
pickFirstValidRoot // endDefinition;
75+
76+
(* ::**************************************************************************************************************:: *)
77+
(* ::Section::Closed:: *)
78+
(*rootURIToPath*)
79+
rootURIToPath // beginDefinition;
80+
rootURIToPath[ uri_String /; StringStartsQ[ uri, "file://" ] ] := ExpandFileName @ LocalObject @ normalizeFileURI @ uri;
81+
rootURIToPath[ _ ] := None;
82+
rootURIToPath // endDefinition;
83+
84+
(* ::**************************************************************************************************************:: *)
85+
(* ::Section::Closed:: *)
86+
(*normalizeFileURI*)
87+
(* Some clients (e.g. Claude Code on Windows) emit malformed file:// URIs containing
88+
backslashes and only two slashes before a drive letter, like
89+
"file://H:\\Documents\\AgentTools". Convert those to a well-formed
90+
"file:///H:/Documents/AgentTools" so LocalObject can decode them correctly. *)
91+
normalizeFileURI // beginDefinition;
92+
93+
normalizeFileURI[ uri_String ] :=
94+
StringReplace[
95+
StringReplace[ uri, "\\" -> "/" ],
96+
StartOfString ~~ "file://" ~~ c: LetterCharacter ~~ ":/" :> "file:///" <> c <> ":/"
97+
];
98+
99+
normalizeFileURI // endDefinition;
100+
101+
(* ::**************************************************************************************************************:: *)
102+
(* ::Section::Closed:: *)
103+
(*applyMCPRoot*)
104+
applyMCPRoot // beginDefinition;
105+
106+
applyMCPRoot[ root_String ] := (
107+
$mcpRoot = root;
108+
SetDirectory @ root;
109+
If[ toolOptionValue[ "WolframLanguageEvaluator", "Method" ] === "Local",
110+
useEvaluatorKernel @ SetDirectory @ root
111+
];
112+
writeLog[ "RootApplied" -> root ];
113+
);
114+
115+
applyMCPRoot // endDefinition;
116+
117+
(* ::**************************************************************************************************************:: *)
118+
(* ::Section::Closed:: *)
119+
(*Package Footer*)
120+
addToMXInitialization[
121+
$clientSupportsRoots = False;
122+
$mcpRoot = None;
123+
];
124+
125+
End[ ];
126+
EndPackage[ ];

Kernel/Main.wl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ $AgentToolsContexts = {
6666
"Wolfram`AgentTools`Formatting`",
6767
"Wolfram`AgentTools`Graphics`",
6868
"Wolfram`AgentTools`InstallMCPServer`",
69+
"Wolfram`AgentTools`MCPClientRequests`",
70+
"Wolfram`AgentTools`MCPRoots`",
6971
"Wolfram`AgentTools`MCPServerObject`",
7072
"Wolfram`AgentTools`PacletExtension`",
7173
"Wolfram`AgentTools`Prompts`",

Kernel/StartMCPServer.wl

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,14 @@ processRequest[ ] :=
494494
message = ConfirmBy[ Developer`ReadRawJSONString @ stdin, AssociationQ ];
495495
writeLog[ "Request" -> message ];
496496
method = Lookup[ message, "method", None ];
497-
id = Lookup[ message, "id", Null ];
497+
id = Lookup[ message, "id", Null ];
498+
499+
(* Response to one of our outstanding server-to-client requests *)
500+
If[ method === None && StringQ @ id && KeyExistsQ[ $mcpClientRequests, id ],
501+
handleClientResponse[ id, message ];
502+
Throw @ Null
503+
];
504+
498505
req = <| "jsonrpc" -> "2.0", "id" -> id |>;
499506
response = catchAlways @ handleMethod[ method, message, req ];
500507
If[ method === "tools/list", $warmupTools = True ];
@@ -513,11 +520,10 @@ processRequest // endDefinition;
513520
(*handleMethod*)
514521
handleMethod // beginDefinition;
515522

516-
(* TODO: if the client supports roots, we should query for them and set directory appropriately
517-
https://modelcontextprotocol.io/specification/2025-11-25/client/roots#protocol-messages *)
518523
handleMethod[ "initialize", msg_, req_ ] := (
519524
$clientName = Replace[ msg[[ "params", "clientInfo", "name" ]], Except[ _String ] :> None ];
520525
$clientSupportsUI = mcpAppsEnabledQ[ ] && clientSupportsUIQ @ msg;
526+
$clientSupportsRoots = ! MissingQ @ msg[ "params", "capabilities", "roots" ];
521527
If[ ! stderrEnabledQ[ ], $Messages = { } ];
522528
<| req, "result" -> initResponse[ $currentMCPServer, msg ] |>
523529
);
@@ -530,8 +536,12 @@ handleMethod[ "prompts/get" , msg_, req_ ] := <| req, "result" -> getPrompt[ m
530536
handleMethod[ "tools/list" , msg_, req_ ] := <| req, "result" -> <| "tools" -> withToolUIMetadata @ $toolList |> |>;
531537
handleMethod[ "tools/call" , msg_, req_ ] := <| req, "result" -> evaluateTool[ msg, req ] |>;
532538

533-
(* Ignored *)
534-
handleMethod[ method_String, _, req_ ] /; StringStartsQ[ method, "notifications/" ] := Null;
539+
(* Notifications: dispatch to handleNotification, then drop the response *)
540+
handleMethod[ method_String, msg_, req_ ] /; StringStartsQ[ method, "notifications/" ] := (
541+
handleNotification[ method, msg ];
542+
Null
543+
);
544+
535545
handleMethod[ _, _, KeyValuePattern[ "id" -> Null ] ] := Null;
536546

537547
(* Unknown method *)

Kernel/Tools/TestReport.wl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ testReport[ files: { __String }, timeConstraint: $$size, memoryConstraint: $$siz
9797
memoryConstraintString
9898
};
9999

100-
result = ConfirmBy[ RunProcess @ processArgs, AssociationQ, "Result" ];
100+
result = ConfirmBy[
101+
RunProcess[
102+
processArgs,
103+
ProcessDirectory -> If[ StringQ @ $mcpRoot, $mcpRoot, Inherited ]
104+
],
105+
AssociationQ,
106+
"Result"
107+
];
101108

102109
If[ result[ "ExitCode" ] =!= 0, throwFailure[ "TestKernelFailure" ] ];
103110

Kernel/Tools/Tools.wl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ BeginPackage[ "Wolfram`AgentTools`Tools`" ];
55
(* Symbols shared in Tools subcontexts: *)
66
`$defaultMCPTools;
77
`importMarkdownString;
8-
`useEvaluatorKernel;
98

109
Begin[ "`Private`" ];
1110

0 commit comments

Comments
 (0)