@nextclaw/mcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +452 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextClaw contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { McpServerDefinition, McpTransport, Config, McpServerScope } from '@nextclaw/core';
|
|
2
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
4
|
+
|
|
5
|
+
type McpServerRecord = {
|
|
6
|
+
name: string;
|
|
7
|
+
definition: McpServerDefinition;
|
|
8
|
+
};
|
|
9
|
+
type McpToolCatalogEntry = {
|
|
10
|
+
qualifiedName: string;
|
|
11
|
+
serverName: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
parameters?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
type McpServerWarmResult = {
|
|
17
|
+
name: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
toolCount: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
type McpDoctorReport = {
|
|
23
|
+
name: string;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
transport: McpTransport["type"];
|
|
26
|
+
accessible: boolean;
|
|
27
|
+
toolCount: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
type McpConfigReconcileResult = {
|
|
31
|
+
added: string[];
|
|
32
|
+
removed: string[];
|
|
33
|
+
enabled: string[];
|
|
34
|
+
disabled: string[];
|
|
35
|
+
restarted: string[];
|
|
36
|
+
warmed: McpServerWarmResult[];
|
|
37
|
+
};
|
|
38
|
+
type McpCatalogFilter = {
|
|
39
|
+
agentId?: string;
|
|
40
|
+
};
|
|
41
|
+
type McpLifecycleState = {
|
|
42
|
+
record: McpServerRecord;
|
|
43
|
+
tools: McpToolCatalogEntry[];
|
|
44
|
+
clientReady: boolean;
|
|
45
|
+
lastReadyAt?: string;
|
|
46
|
+
lastError?: string;
|
|
47
|
+
};
|
|
48
|
+
declare function normalizeMcpServerName(name: string): string;
|
|
49
|
+
declare function readDefaultAgentId(config: Config): string;
|
|
50
|
+
declare function isServerAccessibleToAgent(params: {
|
|
51
|
+
config: Config;
|
|
52
|
+
scope: McpServerScope;
|
|
53
|
+
agentId?: string;
|
|
54
|
+
}): boolean;
|
|
55
|
+
declare function buildQualifiedMcpToolName(serverName: string, toolName: string): string;
|
|
56
|
+
declare function sanitizeMcpSegment(value: string): string;
|
|
57
|
+
|
|
58
|
+
declare function listMcpServers(config: Config): McpServerRecord[];
|
|
59
|
+
declare function getMcpServer(config: Config, name: string): McpServerRecord | undefined;
|
|
60
|
+
|
|
61
|
+
type McpClientHandle = {
|
|
62
|
+
client: Client;
|
|
63
|
+
transport: Transport;
|
|
64
|
+
};
|
|
65
|
+
declare class McpClientFactory {
|
|
66
|
+
private readonly _config?;
|
|
67
|
+
constructor(_config?: Config | undefined);
|
|
68
|
+
create(record: McpServerRecord): McpClientHandle;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare class McpServerLifecycleManager {
|
|
72
|
+
private readonly options;
|
|
73
|
+
private readonly pendingConnections;
|
|
74
|
+
private readonly readyConnections;
|
|
75
|
+
constructor(options: {
|
|
76
|
+
getConfig: () => Config;
|
|
77
|
+
clientFactory?: McpClientFactory;
|
|
78
|
+
now?: () => Date;
|
|
79
|
+
});
|
|
80
|
+
warmServer(record: McpServerRecord): Promise<McpLifecycleState>;
|
|
81
|
+
getCachedCatalog(serverName: string): McpToolCatalogEntry[];
|
|
82
|
+
callTool(record: McpServerRecord, toolName: string, args: Record<string, unknown>): Promise<unknown>;
|
|
83
|
+
closeAll(): Promise<void>;
|
|
84
|
+
closeServer(serverName: string): Promise<void>;
|
|
85
|
+
private ensureConnection;
|
|
86
|
+
private connect;
|
|
87
|
+
private stripConnection;
|
|
88
|
+
private normalizeToolResult;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
declare class McpRegistryService {
|
|
92
|
+
private readonly options;
|
|
93
|
+
private readonly lifecycleManager;
|
|
94
|
+
constructor(options: {
|
|
95
|
+
getConfig: () => Config;
|
|
96
|
+
lifecycleManager?: McpServerLifecycleManager;
|
|
97
|
+
});
|
|
98
|
+
listServers(): McpServerRecord[];
|
|
99
|
+
getServer(name: string): McpServerRecord | undefined;
|
|
100
|
+
prewarmEnabledServers(): Promise<McpServerWarmResult[]>;
|
|
101
|
+
warmServer(name: string): Promise<McpServerWarmResult>;
|
|
102
|
+
reconcileConfig(params: {
|
|
103
|
+
prevConfig: Config;
|
|
104
|
+
nextConfig: Config;
|
|
105
|
+
}): Promise<McpConfigReconcileResult>;
|
|
106
|
+
listAccessibleTools(filter?: McpCatalogFilter): McpToolCatalogEntry[];
|
|
107
|
+
callTool(params: {
|
|
108
|
+
serverName: string;
|
|
109
|
+
toolName: string;
|
|
110
|
+
args: Record<string, unknown>;
|
|
111
|
+
}): Promise<unknown>;
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
private warmRecord;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
declare class McpDoctorService {
|
|
117
|
+
private readonly options;
|
|
118
|
+
constructor(options: {
|
|
119
|
+
getConfig: () => Config;
|
|
120
|
+
registryService?: McpRegistryService;
|
|
121
|
+
});
|
|
122
|
+
inspect(name?: string): Promise<McpDoctorReport[]>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { type McpCatalogFilter, McpClientFactory, type McpConfigReconcileResult, type McpDoctorReport, McpDoctorService, type McpLifecycleState, McpRegistryService, McpServerLifecycleManager, type McpServerRecord, type McpServerWarmResult, type McpToolCatalogEntry, buildQualifiedMcpToolName, getMcpServer, isServerAccessibleToAgent, listMcpServers, normalizeMcpServerName, readDefaultAgentId, sanitizeMcpSegment };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function normalizeMcpServerName(name) {
|
|
3
|
+
const normalized = name.trim();
|
|
4
|
+
if (!normalized) {
|
|
5
|
+
throw new Error("MCP server name is required.");
|
|
6
|
+
}
|
|
7
|
+
if (!/^[A-Za-z0-9._-]+$/.test(normalized)) {
|
|
8
|
+
throw new Error("MCP server name may only contain letters, numbers, dot, underscore, and dash.");
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
function readDefaultAgentId(config) {
|
|
13
|
+
return config.agents.list.find((entry) => entry.default)?.id?.trim() || config.agents.list[0]?.id?.trim() || "main";
|
|
14
|
+
}
|
|
15
|
+
function isServerAccessibleToAgent(params) {
|
|
16
|
+
if (params.scope.allAgents) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const targetAgentId = params.agentId?.trim() || readDefaultAgentId(params.config);
|
|
20
|
+
const explicitAgents = params.scope.agents.map((agentId) => agentId.trim()).filter(Boolean);
|
|
21
|
+
if (explicitAgents.length > 0) {
|
|
22
|
+
return explicitAgents.includes(targetAgentId);
|
|
23
|
+
}
|
|
24
|
+
return targetAgentId === readDefaultAgentId(params.config);
|
|
25
|
+
}
|
|
26
|
+
function buildQualifiedMcpToolName(serverName, toolName) {
|
|
27
|
+
return `mcp_${sanitizeMcpSegment(serverName)}__${sanitizeMcpSegment(toolName)}`;
|
|
28
|
+
}
|
|
29
|
+
function sanitizeMcpSegment(value) {
|
|
30
|
+
const sanitized = value.trim().replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
31
|
+
return sanitized || "unnamed";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/config/mcp-config-normalizer.ts
|
|
35
|
+
function listMcpServers(config) {
|
|
36
|
+
return Object.entries(config.mcp.servers).map(([name, definition]) => ({
|
|
37
|
+
name: normalizeMcpServerName(name),
|
|
38
|
+
definition
|
|
39
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
40
|
+
}
|
|
41
|
+
function getMcpServer(config, name) {
|
|
42
|
+
const normalizedName = normalizeMcpServerName(name);
|
|
43
|
+
const definition = config.mcp.servers[normalizedName];
|
|
44
|
+
if (!definition) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
name: normalizedName,
|
|
49
|
+
definition
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/client/mcp-client-factory.ts
|
|
54
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
55
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
56
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
57
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
58
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
59
|
+
function createClient() {
|
|
60
|
+
return new Client({
|
|
61
|
+
name: "nextclaw-mcp-client",
|
|
62
|
+
version: "0.1.0"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function buildFetch(transport) {
|
|
66
|
+
const timeoutMs = transport.timeoutMs;
|
|
67
|
+
const insecureAgent = transport.verifyTls ? null : new Agent({
|
|
68
|
+
connect: {
|
|
69
|
+
rejectUnauthorized: false
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return async (input, init) => {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
75
|
+
try {
|
|
76
|
+
return await undiciFetch(input, {
|
|
77
|
+
...init,
|
|
78
|
+
dispatcher: insecureAgent ?? init?.dispatcher,
|
|
79
|
+
signal: controller.signal
|
|
80
|
+
});
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createStdioTransport(transport) {
|
|
87
|
+
return new StdioClientTransport({
|
|
88
|
+
command: transport.command,
|
|
89
|
+
args: transport.args,
|
|
90
|
+
cwd: transport.cwd,
|
|
91
|
+
env: Object.keys(transport.env).length > 0 ? transport.env : void 0,
|
|
92
|
+
stderr: transport.stderr
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function createHttpTransport(transport) {
|
|
96
|
+
return new StreamableHTTPClientTransport(new URL(transport.url), {
|
|
97
|
+
requestInit: {
|
|
98
|
+
headers: transport.headers
|
|
99
|
+
},
|
|
100
|
+
fetch: buildFetch(transport)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function createSseTransport(transport) {
|
|
104
|
+
return new SSEClientTransport(new URL(transport.url), {
|
|
105
|
+
requestInit: {
|
|
106
|
+
headers: transport.headers
|
|
107
|
+
},
|
|
108
|
+
eventSourceInit: {
|
|
109
|
+
fetch: buildFetch(transport),
|
|
110
|
+
headers: transport.headers
|
|
111
|
+
},
|
|
112
|
+
fetch: buildFetch(transport)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
var McpClientFactory = class {
|
|
116
|
+
constructor(_config) {
|
|
117
|
+
this._config = _config;
|
|
118
|
+
void this._config;
|
|
119
|
+
}
|
|
120
|
+
create(record) {
|
|
121
|
+
const transport = (() => {
|
|
122
|
+
switch (record.definition.transport.type) {
|
|
123
|
+
case "stdio":
|
|
124
|
+
return createStdioTransport(record.definition.transport);
|
|
125
|
+
case "http":
|
|
126
|
+
return createHttpTransport(record.definition.transport);
|
|
127
|
+
case "sse":
|
|
128
|
+
return createSseTransport(record.definition.transport);
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unsupported MCP transport: ${String(record.definition.transport.type)}`);
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return {
|
|
134
|
+
client: createClient(),
|
|
135
|
+
transport
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/lifecycle/mcp-server-lifecycle-manager.ts
|
|
141
|
+
var McpServerLifecycleManager = class {
|
|
142
|
+
constructor(options) {
|
|
143
|
+
this.options = options;
|
|
144
|
+
}
|
|
145
|
+
pendingConnections = /* @__PURE__ */ new Map();
|
|
146
|
+
readyConnections = /* @__PURE__ */ new Map();
|
|
147
|
+
async warmServer(record) {
|
|
148
|
+
const connection = await this.ensureConnection(record);
|
|
149
|
+
return this.stripConnection(connection);
|
|
150
|
+
}
|
|
151
|
+
getCachedCatalog(serverName) {
|
|
152
|
+
return this.readyConnections.get(serverName)?.tools ?? [];
|
|
153
|
+
}
|
|
154
|
+
async callTool(record, toolName, args) {
|
|
155
|
+
const connection = await this.ensureConnection(record);
|
|
156
|
+
const result = await connection.client.callTool({
|
|
157
|
+
name: toolName,
|
|
158
|
+
arguments: args
|
|
159
|
+
});
|
|
160
|
+
return this.normalizeToolResult(result);
|
|
161
|
+
}
|
|
162
|
+
async closeAll() {
|
|
163
|
+
const pending = await Promise.allSettled(this.pendingConnections.values());
|
|
164
|
+
for (const result of pending) {
|
|
165
|
+
if (result.status === "fulfilled") {
|
|
166
|
+
this.readyConnections.set(result.value.record.name, result.value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const connection of this.readyConnections.values()) {
|
|
170
|
+
await connection.transport.close().catch(() => {
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
this.pendingConnections.clear();
|
|
174
|
+
this.readyConnections.clear();
|
|
175
|
+
}
|
|
176
|
+
async closeServer(serverName) {
|
|
177
|
+
const ready = this.readyConnections.get(serverName);
|
|
178
|
+
this.readyConnections.delete(serverName);
|
|
179
|
+
const pending = this.pendingConnections.get(serverName);
|
|
180
|
+
this.pendingConnections.delete(serverName);
|
|
181
|
+
if (ready) {
|
|
182
|
+
await ready.transport.close().catch(() => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (!pending) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const result = await Promise.allSettled([pending]);
|
|
189
|
+
const settled = result[0];
|
|
190
|
+
if (settled?.status === "fulfilled") {
|
|
191
|
+
await settled.value.transport.close().catch(() => {
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
this.readyConnections.delete(serverName);
|
|
195
|
+
this.pendingConnections.delete(serverName);
|
|
196
|
+
}
|
|
197
|
+
async ensureConnection(record) {
|
|
198
|
+
const ready = this.readyConnections.get(record.name);
|
|
199
|
+
if (ready) {
|
|
200
|
+
return ready;
|
|
201
|
+
}
|
|
202
|
+
const existing = this.pendingConnections.get(record.name);
|
|
203
|
+
if (existing) {
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
const task = this.connect(record);
|
|
207
|
+
this.pendingConnections.set(record.name, task);
|
|
208
|
+
try {
|
|
209
|
+
const connection = await task;
|
|
210
|
+
this.readyConnections.set(record.name, connection);
|
|
211
|
+
this.pendingConnections.delete(record.name);
|
|
212
|
+
return connection;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.pendingConnections.delete(record.name);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async connect(record) {
|
|
219
|
+
const config = this.options.getConfig();
|
|
220
|
+
const clientFactory = this.options.clientFactory ?? new McpClientFactory(config);
|
|
221
|
+
const { client, transport } = clientFactory.create(record);
|
|
222
|
+
await client.connect(transport);
|
|
223
|
+
const listedTools = await client.listTools();
|
|
224
|
+
const tools = listedTools.tools.map((tool) => ({
|
|
225
|
+
qualifiedName: buildQualifiedMcpToolName(record.name, tool.name),
|
|
226
|
+
serverName: record.name,
|
|
227
|
+
toolName: tool.name,
|
|
228
|
+
description: tool.description,
|
|
229
|
+
parameters: tool.inputSchema
|
|
230
|
+
}));
|
|
231
|
+
const now = (this.options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
232
|
+
return {
|
|
233
|
+
record,
|
|
234
|
+
tools,
|
|
235
|
+
clientReady: true,
|
|
236
|
+
lastReadyAt: now,
|
|
237
|
+
client,
|
|
238
|
+
transport
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
stripConnection(connection) {
|
|
242
|
+
return {
|
|
243
|
+
record: connection.record,
|
|
244
|
+
tools: connection.tools,
|
|
245
|
+
clientReady: connection.clientReady,
|
|
246
|
+
lastReadyAt: connection.lastReadyAt,
|
|
247
|
+
lastError: connection.lastError
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
normalizeToolResult(result) {
|
|
251
|
+
if ("toolResult" in result) {
|
|
252
|
+
return result.toolResult;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
content: result.content,
|
|
256
|
+
structuredContent: result.structuredContent,
|
|
257
|
+
isError: result.isError ?? false
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/registry/mcp-registry-service.ts
|
|
263
|
+
var McpRegistryService = class {
|
|
264
|
+
constructor(options) {
|
|
265
|
+
this.options = options;
|
|
266
|
+
this.lifecycleManager = this.options.lifecycleManager ?? new McpServerLifecycleManager({
|
|
267
|
+
getConfig: this.options.getConfig
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
lifecycleManager;
|
|
271
|
+
listServers() {
|
|
272
|
+
return listMcpServers(this.options.getConfig());
|
|
273
|
+
}
|
|
274
|
+
getServer(name) {
|
|
275
|
+
return getMcpServer(this.options.getConfig(), name);
|
|
276
|
+
}
|
|
277
|
+
async prewarmEnabledServers() {
|
|
278
|
+
const results = await Promise.all(
|
|
279
|
+
this.listServers().filter((record) => record.definition.enabled).map(async (record) => {
|
|
280
|
+
try {
|
|
281
|
+
const state = await this.lifecycleManager.warmServer(record);
|
|
282
|
+
return {
|
|
283
|
+
name: record.name,
|
|
284
|
+
ok: true,
|
|
285
|
+
toolCount: state.tools.length
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
name: record.name,
|
|
290
|
+
ok: false,
|
|
291
|
+
toolCount: 0,
|
|
292
|
+
error: toErrorMessage(error)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
return results.sort((left, right) => left.name.localeCompare(right.name));
|
|
298
|
+
}
|
|
299
|
+
async warmServer(name) {
|
|
300
|
+
const server = this.getServer(name);
|
|
301
|
+
if (!server) {
|
|
302
|
+
throw new Error(`Unknown MCP server: ${name}`);
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const state = await this.lifecycleManager.warmServer(server);
|
|
306
|
+
return {
|
|
307
|
+
name: server.name,
|
|
308
|
+
ok: true,
|
|
309
|
+
toolCount: state.tools.length
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
name: server.name,
|
|
314
|
+
ok: false,
|
|
315
|
+
toolCount: 0,
|
|
316
|
+
error: toErrorMessage(error)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async reconcileConfig(params) {
|
|
321
|
+
const previousServers = new Map(listMcpServers(params.prevConfig).map((record) => [record.name, record]));
|
|
322
|
+
const nextServers = new Map(listMcpServers(params.nextConfig).map((record) => [record.name, record]));
|
|
323
|
+
const serverNames = Array.from(/* @__PURE__ */ new Set([...previousServers.keys(), ...nextServers.keys()])).sort(
|
|
324
|
+
(left, right) => left.localeCompare(right)
|
|
325
|
+
);
|
|
326
|
+
const result = {
|
|
327
|
+
added: [],
|
|
328
|
+
removed: [],
|
|
329
|
+
enabled: [],
|
|
330
|
+
disabled: [],
|
|
331
|
+
restarted: [],
|
|
332
|
+
warmed: []
|
|
333
|
+
};
|
|
334
|
+
for (const serverName of serverNames) {
|
|
335
|
+
const previous = previousServers.get(serverName);
|
|
336
|
+
const next = nextServers.get(serverName);
|
|
337
|
+
if (!next) {
|
|
338
|
+
await this.lifecycleManager.closeServer(serverName);
|
|
339
|
+
result.removed.push(serverName);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!previous) {
|
|
343
|
+
result.added.push(serverName);
|
|
344
|
+
if (next.definition.enabled) {
|
|
345
|
+
result.warmed.push(await this.warmRecord(next));
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (previous.definition.enabled && !next.definition.enabled) {
|
|
350
|
+
await this.lifecycleManager.closeServer(serverName);
|
|
351
|
+
result.disabled.push(serverName);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!previous.definition.enabled && next.definition.enabled) {
|
|
355
|
+
result.enabled.push(serverName);
|
|
356
|
+
result.warmed.push(await this.warmRecord(next));
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (hasReconnectRelevantChange(previous.definition, next.definition)) {
|
|
360
|
+
await this.lifecycleManager.closeServer(serverName);
|
|
361
|
+
result.restarted.push(serverName);
|
|
362
|
+
if (next.definition.enabled) {
|
|
363
|
+
result.warmed.push(await this.warmRecord(next));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
listAccessibleTools(filter = {}) {
|
|
370
|
+
const config = this.options.getConfig();
|
|
371
|
+
return this.listServers().filter((record) => record.definition.enabled).filter(
|
|
372
|
+
(record) => isServerAccessibleToAgent({
|
|
373
|
+
config,
|
|
374
|
+
scope: record.definition.scope,
|
|
375
|
+
agentId: filter.agentId
|
|
376
|
+
})
|
|
377
|
+
).flatMap((record) => this.lifecycleManager.getCachedCatalog(record.name)).sort((left, right) => left.qualifiedName.localeCompare(right.qualifiedName));
|
|
378
|
+
}
|
|
379
|
+
async callTool(params) {
|
|
380
|
+
const server = this.getServer(params.serverName);
|
|
381
|
+
if (!server) {
|
|
382
|
+
throw new Error(`Unknown MCP server: ${params.serverName}`);
|
|
383
|
+
}
|
|
384
|
+
return this.lifecycleManager.callTool(server, params.toolName, params.args);
|
|
385
|
+
}
|
|
386
|
+
async close() {
|
|
387
|
+
await this.lifecycleManager.closeAll();
|
|
388
|
+
}
|
|
389
|
+
async warmRecord(record) {
|
|
390
|
+
try {
|
|
391
|
+
const state = await this.lifecycleManager.warmServer(record);
|
|
392
|
+
return {
|
|
393
|
+
name: record.name,
|
|
394
|
+
ok: true,
|
|
395
|
+
toolCount: state.tools.length
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return {
|
|
399
|
+
name: record.name,
|
|
400
|
+
ok: false,
|
|
401
|
+
toolCount: 0,
|
|
402
|
+
error: toErrorMessage(error)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function toErrorMessage(error) {
|
|
408
|
+
return error instanceof Error ? error.message : String(error);
|
|
409
|
+
}
|
|
410
|
+
function hasReconnectRelevantChange(left, right) {
|
|
411
|
+
return JSON.stringify(left.transport) !== JSON.stringify(right.transport);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/doctor/mcp-doctor-service.ts
|
|
415
|
+
var McpDoctorService = class {
|
|
416
|
+
constructor(options) {
|
|
417
|
+
this.options = options;
|
|
418
|
+
}
|
|
419
|
+
async inspect(name) {
|
|
420
|
+
const registry = this.options.registryService ?? new McpRegistryService({
|
|
421
|
+
getConfig: this.options.getConfig
|
|
422
|
+
});
|
|
423
|
+
const specificServer = name ? registry.getServer(name) : void 0;
|
|
424
|
+
const servers = specificServer ? [specificServer] : name ? [] : registry.listServers();
|
|
425
|
+
return await Promise.all(
|
|
426
|
+
servers.map(async (server) => {
|
|
427
|
+
const warmResult = await registry.warmServer(server.name);
|
|
428
|
+
return {
|
|
429
|
+
name: server.name,
|
|
430
|
+
enabled: server.definition.enabled,
|
|
431
|
+
transport: server.definition.transport.type,
|
|
432
|
+
accessible: warmResult.ok,
|
|
433
|
+
toolCount: warmResult.toolCount,
|
|
434
|
+
...warmResult.error ? { error: warmResult.error } : {}
|
|
435
|
+
};
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
export {
|
|
441
|
+
McpClientFactory,
|
|
442
|
+
McpDoctorService,
|
|
443
|
+
McpRegistryService,
|
|
444
|
+
McpServerLifecycleManager,
|
|
445
|
+
buildQualifiedMcpToolName,
|
|
446
|
+
getMcpServer,
|
|
447
|
+
isServerAccessibleToAgent,
|
|
448
|
+
listMcpServers,
|
|
449
|
+
normalizeMcpServerName,
|
|
450
|
+
readDefaultAgentId,
|
|
451
|
+
sanitizeMcpSegment
|
|
452
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextclaw/mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Platform-level MCP registry, lifecycle, and diagnostics for NextClaw.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"development": "./src/index.ts",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
19
|
+
"undici": "^6.21.0",
|
|
20
|
+
"@nextclaw/core": "0.9.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.17.6",
|
|
24
|
+
"prettier": "^3.3.3",
|
|
25
|
+
"tsup": "^8.3.5",
|
|
26
|
+
"typescript": "^5.6.3",
|
|
27
|
+
"vitest": "^2.1.2",
|
|
28
|
+
"zod": "^4.3.6"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist",
|
|
32
|
+
"lint": "cd ../.. && pnpm exec eslint packages/nextclaw-mcp --config eslint.config.mjs",
|
|
33
|
+
"tsc": "tsc -p tsconfig.json",
|
|
34
|
+
"test": "vitest run"
|
|
35
|
+
}
|
|
36
|
+
}
|