expose the clide command surface over MCP (T-225)
External MCP clients (Cursor, Windsurf, Copilot) can now drive clide. The MCP server's tools/list is generated from the co-registered command+schema registry (D-74) that already feeds the CLI and palette — the full mcp__clide__* namespace with no hand-maintained second surface (D-86). Each command's CommandSchema maps to a JSON-Schema inputSchema; tools/call routes mcp__clide__<cmd> to dispatcher.dispatch and renders the IpcResponse as MCP content (data as JSON, errors with isError). register() gains a mcpExpose flag (default true); pane.tail opts out as a poor request/response fit. tail/events are server-intercepted so they're naturally absent. The two /ide stubs (getDiagnostics, executeCode) are left as stubs — making them real (analyzer hook, Jupyter eval) is out of scope per the ticket. Transport unchanged (SSE, D-73). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,13 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/kernel/src/log.dart';
|
||||
import 'package:clide/src/daemon/dispatcher.dart';
|
||||
import 'package:clide/src/ipc/envelope.dart';
|
||||
|
||||
/// Prefix for clide's own MCP tools — the full command surface, generated
|
||||
/// from the dispatcher registry (D-86). The `mcp__ide__*` pair is the
|
||||
/// separate `/ide` minimum (D-68).
|
||||
const String _clideToolPrefix = 'mcp__clide__';
|
||||
|
||||
/// One connected SSE client. Each session has its own response
|
||||
/// stream; POST /messages routes back to the right one via the
|
||||
@@ -59,6 +66,7 @@ class McpServer {
|
||||
McpServer({
|
||||
required this.workspaceRoot,
|
||||
required this.log,
|
||||
this.dispatcher,
|
||||
this.discoveryDirOverride,
|
||||
this.bindHost = '127.0.0.1',
|
||||
this.bindPort = 0,
|
||||
@@ -69,6 +77,11 @@ class McpServer {
|
||||
final String workspaceRoot;
|
||||
final Logger log;
|
||||
|
||||
/// The command dispatcher whose registry drives the `mcp__clide__*` tool
|
||||
/// surface and handles `tools/call` (D-86). Null → only the `/ide` minimum
|
||||
/// tools are served (tests that don't need the full surface).
|
||||
final DaemonDispatcher? dispatcher;
|
||||
|
||||
/// Override of `$HOME/.claude/ide/` for tests. Production code
|
||||
/// passes null; tests inject a tempdir.
|
||||
final String? discoveryDirOverride;
|
||||
@@ -222,6 +235,9 @@ class McpServer {
|
||||
case 'tools/list':
|
||||
return {
|
||||
'tools': [
|
||||
// The `/ide` minimum (D-68). Both still stubbed — getDiagnostics
|
||||
// and executeCode (Jupyter) are deferred follow-ups; T-225 wires
|
||||
// the clide command surface below.
|
||||
{
|
||||
'name': 'mcp__ide__getDiagnostics',
|
||||
'description': 'Return diagnostics from the open editor (stubbed).',
|
||||
@@ -242,10 +258,15 @@ class McpServer {
|
||||
},
|
||||
},
|
||||
},
|
||||
// The full clide surface, generated from the command registry.
|
||||
...?dispatcher?.mcpTools(),
|
||||
],
|
||||
};
|
||||
case 'tools/call':
|
||||
final name = (params?['name'] as String?) ?? '';
|
||||
if (name.startsWith(_clideToolPrefix)) {
|
||||
return _callClideTool(name, params);
|
||||
}
|
||||
switch (name) {
|
||||
case 'mcp__ide__getDiagnostics':
|
||||
return {
|
||||
@@ -268,6 +289,39 @@ class McpServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a `mcp__clide__<cmd>` tool by dispatching the underlying command
|
||||
/// (D-86). The MCP `arguments` object maps straight to the request's named
|
||||
/// args — the dispatcher's D-74 schema normalises/validates them. The
|
||||
/// response is rendered as MCP tool content: `data` as JSON text on success,
|
||||
/// the error message with `isError: true` on failure.
|
||||
Future<Object?> _callClideTool(String name, Map<String, Object?>? params) async {
|
||||
final d = dispatcher;
|
||||
if (d == null) {
|
||||
return {
|
||||
'content': [
|
||||
{'type': 'text', 'text': 'clide command surface is not available'},
|
||||
],
|
||||
'isError': true,
|
||||
};
|
||||
}
|
||||
final cmd = name.substring(_clideToolPrefix.length);
|
||||
final args = (params?['arguments'] as Map?)?.cast<String, Object?>() ?? const {};
|
||||
final resp = await d.dispatch(IpcRequest(id: 'mcp', cmd: cmd, args: args));
|
||||
if (resp.ok) {
|
||||
return {
|
||||
'content': [
|
||||
{'type': 'text', 'text': jsonEncode(resp.data)},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
'content': [
|
||||
{'type': 'text', 'text': resp.error?.message ?? 'command failed'},
|
||||
],
|
||||
'isError': true,
|
||||
};
|
||||
}
|
||||
|
||||
// -- discovery file -------------------------------------------------------
|
||||
|
||||
Future<String> _writeDiscoveryFile() async {
|
||||
|
||||
Reference in New Issue
Block a user