Skip to content

Commit 87ff7e2

Browse files
committed
fix(mcp): push list_changed when host catalog reloads
Daemon ConnectedHostCatalog stayed stale after Reload/AddPath in Revit, so search_dynamic could not see newly registered tools until reconnect. Host now broadcasts tools/list_changed and resources/list_changed to connected MCP pipe clients. - Add McpJsonRpc.CreateNotification and McpPipeSession.SendNotificationAsync - Refactor McpPipeSession read loop; remove unused RunAsync - Add NamedPipe test for tool list change notifications - Add mcp_autonomous sample for PostCommand probing
1 parent 0aa81ba commit 87ff7e2

6 files changed

Lines changed: 207 additions & 131 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from mcp.server.mcpserver import MCPServer
2+
from mcp.types import CallToolResult, TextContent, ToolAnnotations
3+
from typing import Annotated
4+
from pydantic import Field
5+
6+
from RevitDevTool.Core import RevitContext
7+
from Autodesk.Revit import UI
8+
9+
mcp = MCPServer("Autonomous Revit Toolset")
10+
11+
12+
def register_autonomous_tools(mcp: MCPServer) -> None:
13+
14+
@mcp.tool(annotations=ToolAnnotations(title="Probe / Post Revit Command"))
15+
async def autonomous_post_command(
16+
command_id: Annotated[str, Field(description="Revit command id for LookupCommandId")],
17+
post: Annotated[
18+
bool,
19+
Field(description="If true, call PostCommand when CanPostCommand is true"),
20+
] = False,
21+
) -> CallToolResult:
22+
"""Temporary spike: probe LookupCommandId / CanPostCommand / PostCommand."""
23+
24+
uiapp = RevitContext.UiApplication
25+
cmd = UI.RevitCommandId.LookupCommandId(command_id)
26+
found = cmd is not None
27+
can_post = uiapp.CanPostCommand(cmd) if found else False
28+
29+
lines = [
30+
f"command_id={command_id}",
31+
f"found={found}",
32+
f"can_post={can_post}",
33+
]
34+
if found:
35+
lines.append(f"name={cmd.Name}")
36+
lines.append(f"id={cmd.Id}")
37+
38+
if post and can_post:
39+
uiapp.PostCommand(cmd)
40+
lines.append("posted=true")
41+
elif post:
42+
lines.append("posted=false (CanPostCommand was false)")
43+
44+
return CallToolResult(content=[TextContent(type="text", text="\n".join(lines))])
45+
46+
47+
register_autonomous_tools(mcp)

source/DevTools.Mcp.Adapter/External/HostMcpPipeServer.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using DevTools.Mcp.Core.Protocol;
77
using Microsoft.Extensions.Hosting;
88
using Microsoft.Extensions.Logging;
9+
using ModelContextProtocol.Protocol;
910
using ZLogger;
1011
namespace DevTools.Mcp.Adapter.External;
1112

@@ -27,7 +28,7 @@ public sealed class HostMcpPipeServer(
2728

2829
private CancellationTokenSource? _cts;
2930
private Task? _acceptLoopTask;
30-
private readonly ConcurrentDictionary<int, IAsyncDisposable> _sessions = new();
31+
private readonly ConcurrentDictionary<int, McpPipeSession> _sessions = new();
3132
private int _nextSessionId;
3233
private string? _pipeName;
3334
private bool _disposed;
@@ -111,12 +112,11 @@ private async Task AcceptLoopAsync(CancellationToken ct)
111112
private async Task HandleConnectionAsync(NamedPipeServerStream pipe, CancellationToken ct)
112113
{
113114
var sessionId = Interlocked.Increment(ref _nextSessionId);
114-
IAsyncDisposable? session = null;
115+
McpPipeSession? pipeSession = null;
115116
try
116117
{
117-
var pipeSession = McpPipeSession.Start(pipe, mcpHandler, ct);
118-
session = pipeSession;
119-
_sessions[sessionId] = session;
118+
pipeSession = McpPipeSession.Start(pipe, mcpHandler, ct);
119+
_sessions[sessionId] = pipeSession;
120120
connectionTracker.SetMcpClientCount(_sessions.Count);
121121

122122
logger.ZLogInformation($"MCP client connected. Active sessions: {_sessions.Count}");
@@ -129,10 +129,10 @@ private async Task HandleConnectionAsync(NamedPipeServerStream pipe, Cancellatio
129129
}
130130
finally
131131
{
132-
if (session is not null)
132+
if (pipeSession is not null)
133133
{
134134
_sessions.TryRemove(sessionId, out _);
135-
await session.DisposeAsync().ConfigureAwait(false);
135+
await pipeSession.DisposeAsync().ConfigureAwait(false);
136136
}
137137
else
138138
{
@@ -150,6 +150,7 @@ private void OnCatalogChanged(object? sender, EventArgs e)
150150
{
151151
primitiveDispatcher.ClearCaches();
152152
toolsetContextManager.Clear();
153+
_ = BroadcastCatalogListChangedNotificationsAsync();
153154
logger.ZLogInformation(
154155
$"MCP host catalog reloaded ({catalogStore.ToolDescriptors.Count} tools, {catalogStore.ResourceDescriptors.Count} resources).");
155156
}
@@ -159,6 +160,22 @@ private void OnCatalogChanged(object? sender, EventArgs e)
159160
}
160161
}
161162

163+
private async Task BroadcastCatalogListChangedNotificationsAsync()
164+
{
165+
foreach (var session in _sessions.Values)
166+
{
167+
try
168+
{
169+
await session.SendNotificationAsync(NotificationMethods.ToolListChangedNotification).ConfigureAwait(false);
170+
await session.SendNotificationAsync(NotificationMethods.ResourceListChangedNotification).ConfigureAwait(false);
171+
}
172+
catch (Exception ex)
173+
{
174+
logger.ZLogWarning($"Failed to notify MCP client of catalog change: {ex.Message}");
175+
}
176+
}
177+
}
178+
162179
private static NamedPipeServerStream CreateServerPipe(string pipeName)
163180
{
164181
var security = new PipeSecurity();
Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,40 @@
11
using System.Text.Json;
22
using System.Text.Json.Nodes;
3-
4-
using DevTools.Mcp.Core.Protocol;
5-
63
using JsonRpcKeys = DevTools.Mcp.Core.Protocol.McpSpecKeys.JsonRpc;
74

8-
9-
105
namespace DevTools.Mcp.Adapter.Host;
116

12-
13-
147
/// <summary>JSON-RPC 2.0 envelope helpers for the host MCP handler.</summary>
15-
168
internal static class McpJsonRpc
17-
189
{
19-
2010
public const string Version = JsonRpcKeys.Version;
21-
2211
public const int InvalidRequest = JsonRpcKeys.InvalidRequest;
23-
2412
public const int MethodNotFound = JsonRpcKeys.MethodNotFound;
25-
2613
public const int InvalidParams = JsonRpcKeys.InvalidParams;
27-
2814
public const int InternalError = JsonRpcKeys.InternalError;
29-
3015
public const int UnsupportedProtocolVersion = JsonRpcKeys.UnsupportedProtocolVersion;
3116

32-
33-
3417
public static JsonObject CreateSuccess(JsonNode? id, JsonNode result) =>
35-
3618
new()
37-
3819
{
39-
4020
[JsonRpcKeys.Envelope] = Version,
41-
4221
[JsonRpcKeys.Id] = id?.DeepClone(),
43-
4422
[JsonRpcKeys.Result] = result.DeepClone(),
23+
};
4524

25+
public static JsonObject CreateNotification(string method, JsonObject? parameters = null)
26+
{
27+
var notification = new JsonObject
28+
{
29+
[JsonRpcKeys.Envelope] = Version,
30+
[JsonRpcKeys.Method] = method,
4631
};
4732

33+
if (parameters is not null)
34+
notification[JsonRpcKeys.Params] = parameters.DeepClone();
4835

36+
return notification;
37+
}
4938

5039
public static JsonObject CreateError(JsonNode? id, int code, string message) =>
5140
CreateError(id, code, message, data: null);
@@ -69,67 +58,33 @@ public static JsonObject CreateError(JsonNode? id, int code, string message, Jso
6958
},
7059
};
7160

72-
73-
7461
public static bool TryGetMethod(JsonObject request, out string? method)
75-
7662
{
77-
7863
if (request[JsonRpcKeys.Method] is JsonValue value && value.TryGetValue(out string? parsed))
79-
8064
{
81-
8265
method = parsed;
83-
8466
return true;
85-
8667
}
8768

88-
89-
9069
method = null;
91-
9270
return false;
93-
9471
}
9572

96-
97-
9873
public static JsonNode? GetId(JsonObject request) => request[JsonRpcKeys.Id];
9974

100-
101-
10275
public static bool HasId(JsonObject request) => request.ContainsKey(JsonRpcKeys.Id);
10376

104-
105-
10677
public static JsonObject? GetParams(JsonObject request) =>
107-
10878
request[JsonRpcKeys.Params] switch
109-
11079
{
111-
11280
JsonObject obj => obj,
113-
114-
null => null,
115-
11681
_ => null,
117-
11882
};
11983

120-
121-
12284
public static JsonObject ParseRequest(string json) =>
123-
12485
JsonNode.Parse(json)?.AsObject()
125-
12686
?? throw new JsonException("Expected a JSON-RPC request object.");
12787

128-
129-
13088
public static string Serialize(JsonObject message) =>
131-
13289
message.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
133-
13490
}
135-

0 commit comments

Comments
 (0)