@pencil-agent/nano-pencil 1.7.0 → 1.8.0
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/dist/core/mcp/mcp-adapter.js +24 -13
- package/dist/core/mcp/mcp-client.d.ts +14 -4
- package/dist/core/mcp/mcp-client.js +271 -36
- package/dist/core/mcp-manager.js +2 -2
- package/dist/core/sdk.js +4 -5
- package/dist/extensions/nanomem/package.json +5 -0
- package/dist/extensions/nanosoul/package.json +5 -0
- package/dist/main.js +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +25 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/warm.json +81 -0
- package/dist/packages/nanomem/extension.js +55 -6
- package/dist/packages/nanomem/package.json +5 -0
- package/dist/packages/nanosoul/package.json +5 -0
- package/package.json +86 -85
|
@@ -4,25 +4,38 @@
|
|
|
4
4
|
* Adapts MCP tools to work with NanoPencil's tool system.
|
|
5
5
|
*/
|
|
6
6
|
import { formatGuidanceMessage, getAPIKeyGuidance } from "./mcp-guidance.js";
|
|
7
|
+
function toSafeToolName(fullName) {
|
|
8
|
+
const normalized = `mcp_${fullName.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
9
|
+
if (normalized.length <= 60)
|
|
10
|
+
return normalized;
|
|
11
|
+
let hash = 0;
|
|
12
|
+
for (let i = 0; i < fullName.length; i++) {
|
|
13
|
+
hash = (hash * 31 + fullName.charCodeAt(i)) | 0;
|
|
14
|
+
}
|
|
15
|
+
const suffix = Math.abs(hash).toString(36);
|
|
16
|
+
return `${normalized.slice(0, 50)}_${suffix}`;
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
19
|
* Create a NanoPencil ToolDefinition from an MCP tool definition
|
|
9
20
|
*/
|
|
10
21
|
export function createMCPTool(mcpClient, mcpTool) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
22
|
+
const rawToolName = mcpTool.name; // Full name like "filesystem/read"
|
|
23
|
+
const toolName = toSafeToolName(rawToolName);
|
|
24
|
+
const [serverId] = rawToolName.split("/");
|
|
13
25
|
return {
|
|
14
26
|
name: toolName,
|
|
15
|
-
label:
|
|
16
|
-
description: mcpTool.description
|
|
27
|
+
label: rawToolName,
|
|
28
|
+
description: `${mcpTool.description} (MCP: ${rawToolName})`,
|
|
17
29
|
// Use TypeBox Object schema with any properties since MCP tools have dynamic schemas
|
|
18
|
-
parameters:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
parameters: mcpTool.inputSchema ??
|
|
31
|
+
{
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {},
|
|
34
|
+
additionalProperties: true,
|
|
35
|
+
},
|
|
23
36
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
24
37
|
try {
|
|
25
|
-
const result = await mcpClient.callTool(
|
|
38
|
+
const result = await mcpClient.callTool(rawToolName, params);
|
|
26
39
|
if (result.error) {
|
|
27
40
|
// Check if error is due to missing API key and provide guidance
|
|
28
41
|
const guidance = getAPIKeyGuidance(serverId);
|
|
@@ -79,9 +92,7 @@ export function createMCPTool(mcpClient, mcpTool) {
|
|
|
79
92
|
};
|
|
80
93
|
}
|
|
81
94
|
return {
|
|
82
|
-
content: [
|
|
83
|
-
{ type: "text", text: `Failed to call MCP tool ${toolName}` },
|
|
84
|
-
],
|
|
95
|
+
content: [{ type: "text", text: `Failed to call MCP tool ${rawToolName}` }],
|
|
85
96
|
details: {
|
|
86
97
|
error: error instanceof Error ? error.message : String(error),
|
|
87
98
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP (Model Context Protocol) Client
|
|
3
3
|
*
|
|
4
4
|
* Provides client functionality to connect to MCP servers and call their tools.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports stdio transport with JSON-RPC framing.
|
|
6
6
|
*/
|
|
7
7
|
export interface MCPServerConfig {
|
|
8
8
|
/** Unique identifier for this server */
|
|
@@ -28,7 +28,7 @@ export interface MCPTool {
|
|
|
28
28
|
/** Tool description */
|
|
29
29
|
description: string;
|
|
30
30
|
/** JSON Schema for input */
|
|
31
|
-
inputSchema:
|
|
31
|
+
inputSchema: Record<string, unknown>;
|
|
32
32
|
/** Server ID */
|
|
33
33
|
serverId: string;
|
|
34
34
|
}
|
|
@@ -50,7 +50,7 @@ export interface MCPToolResult {
|
|
|
50
50
|
*/
|
|
51
51
|
export declare class MCPClient {
|
|
52
52
|
private servers;
|
|
53
|
-
private
|
|
53
|
+
private serverRuntimes;
|
|
54
54
|
private serverTools;
|
|
55
55
|
constructor();
|
|
56
56
|
/**
|
|
@@ -73,6 +73,16 @@ export declare class MCPClient {
|
|
|
73
73
|
* Remove a server
|
|
74
74
|
*/
|
|
75
75
|
removeServer(id: string): void;
|
|
76
|
+
private getRuntime;
|
|
77
|
+
private attachStdoutParser;
|
|
78
|
+
private processStdoutBuffer;
|
|
79
|
+
private handleJsonRpcMessage;
|
|
80
|
+
private writeFramedMessage;
|
|
81
|
+
private sendNotification;
|
|
82
|
+
private sendRequest;
|
|
83
|
+
private initializeServer;
|
|
84
|
+
private normalizeToolRecord;
|
|
85
|
+
private loadToolsForServer;
|
|
76
86
|
/**
|
|
77
87
|
* Start an MCP server (for stdio transport)
|
|
78
88
|
*/
|
|
@@ -92,7 +102,7 @@ export declare class MCPClient {
|
|
|
92
102
|
/**
|
|
93
103
|
* Call an MCP tool
|
|
94
104
|
*/
|
|
95
|
-
callTool(toolName: string, args: Record<string,
|
|
105
|
+
callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolResult>;
|
|
96
106
|
/**
|
|
97
107
|
* Call tool via stdio (JSON-RPC)
|
|
98
108
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP (Model Context Protocol) Client
|
|
3
3
|
*
|
|
4
4
|
* Provides client functionality to connect to MCP servers and call their tools.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports stdio transport with JSON-RPC framing.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from "child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -14,7 +14,7 @@ import { getAgentDir } from "../../config.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export class MCPClient {
|
|
16
16
|
servers = new Map();
|
|
17
|
-
|
|
17
|
+
serverRuntimes = new Map();
|
|
18
18
|
serverTools = new Map();
|
|
19
19
|
constructor() {
|
|
20
20
|
this.loadServersFromConfig();
|
|
@@ -67,6 +67,202 @@ export class MCPClient {
|
|
|
67
67
|
this.serverTools.delete(id);
|
|
68
68
|
this.stopServer(id);
|
|
69
69
|
}
|
|
70
|
+
getRuntime(serverId) {
|
|
71
|
+
return this.serverRuntimes.get(serverId);
|
|
72
|
+
}
|
|
73
|
+
attachStdoutParser(serverId, runtime) {
|
|
74
|
+
runtime.process.stdout.on("data", (chunk) => {
|
|
75
|
+
runtime.buffer = Buffer.concat([runtime.buffer, chunk]);
|
|
76
|
+
this.processStdoutBuffer(serverId, runtime);
|
|
77
|
+
});
|
|
78
|
+
runtime.process.stderr.on("data", (chunk) => {
|
|
79
|
+
const text = chunk.toString("utf8").trim();
|
|
80
|
+
if (text.length > 0) {
|
|
81
|
+
console.error(`[MCP:${serverId}] ${text}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
runtime.process.on("exit", (code, signal) => {
|
|
85
|
+
const message = `MCP server ${serverId} exited (code=${code ?? "null"}, signal=${signal ?? "null"})`;
|
|
86
|
+
for (const pending of runtime.pendingRequests.values()) {
|
|
87
|
+
clearTimeout(pending.timer);
|
|
88
|
+
pending.reject(new Error(message));
|
|
89
|
+
}
|
|
90
|
+
runtime.pendingRequests.clear();
|
|
91
|
+
this.serverRuntimes.delete(serverId);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
processStdoutBuffer(serverId, runtime) {
|
|
95
|
+
// MCP stdio framing: "Content-Length: N\r\n\r\n<json>"
|
|
96
|
+
while (runtime.buffer.length > 0) {
|
|
97
|
+
const headerEnd = runtime.buffer.indexOf("\r\n\r\n");
|
|
98
|
+
if (headerEnd !== -1) {
|
|
99
|
+
const headerText = runtime.buffer.slice(0, headerEnd).toString("utf8");
|
|
100
|
+
const lengthMatch = headerText.match(/content-length:\s*(\d+)/i);
|
|
101
|
+
if (!lengthMatch) {
|
|
102
|
+
runtime.buffer = runtime.buffer.slice(headerEnd + 4);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const bodyLength = Number(lengthMatch[1]);
|
|
106
|
+
const totalLength = headerEnd + 4 + bodyLength;
|
|
107
|
+
if (runtime.buffer.length < totalLength) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const body = runtime.buffer
|
|
111
|
+
.slice(headerEnd + 4, totalLength)
|
|
112
|
+
.toString("utf8");
|
|
113
|
+
runtime.buffer = runtime.buffer.slice(totalLength);
|
|
114
|
+
this.handleJsonRpcMessage(serverId, body);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Fallback for line-delimited JSON
|
|
118
|
+
const lineEnd = runtime.buffer.indexOf("\n");
|
|
119
|
+
if (lineEnd === -1) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const line = runtime.buffer.slice(0, lineEnd).toString("utf8").trim();
|
|
123
|
+
runtime.buffer = runtime.buffer.slice(lineEnd + 1);
|
|
124
|
+
if (line.length > 0) {
|
|
125
|
+
this.handleJsonRpcMessage(serverId, line);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
handleJsonRpcMessage(serverId, raw) {
|
|
130
|
+
let msg;
|
|
131
|
+
try {
|
|
132
|
+
msg = JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (msg.id === undefined) {
|
|
138
|
+
// Notification/unsolicited message
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const id = typeof msg.id === "number" ? msg.id : Number(msg.id);
|
|
142
|
+
if (!Number.isFinite(id)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const runtime = this.serverRuntimes.get(serverId);
|
|
146
|
+
if (!runtime)
|
|
147
|
+
return;
|
|
148
|
+
const pending = runtime.pendingRequests.get(id);
|
|
149
|
+
if (!pending)
|
|
150
|
+
return;
|
|
151
|
+
clearTimeout(pending.timer);
|
|
152
|
+
runtime.pendingRequests.delete(id);
|
|
153
|
+
if (msg.error) {
|
|
154
|
+
pending.reject(new Error(msg.error.message || `JSON-RPC error ${msg.error.code ?? "unknown"}`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
pending.resolve(msg.result);
|
|
158
|
+
}
|
|
159
|
+
writeFramedMessage(runtime, message) {
|
|
160
|
+
const body = JSON.stringify(message);
|
|
161
|
+
const framed = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
162
|
+
runtime.process.stdin.write(framed);
|
|
163
|
+
}
|
|
164
|
+
sendNotification(serverId, method, params) {
|
|
165
|
+
const runtime = this.getRuntime(serverId);
|
|
166
|
+
if (!runtime)
|
|
167
|
+
return;
|
|
168
|
+
this.writeFramedMessage(runtime, {
|
|
169
|
+
jsonrpc: "2.0",
|
|
170
|
+
method,
|
|
171
|
+
params: params ?? {},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async sendRequest(serverId, method, params, timeoutMs = 20_000) {
|
|
175
|
+
const runtime = this.getRuntime(serverId);
|
|
176
|
+
if (!runtime) {
|
|
177
|
+
throw new Error(`Server ${serverId} is not running`);
|
|
178
|
+
}
|
|
179
|
+
const id = runtime.nextRequestId++;
|
|
180
|
+
const payload = {
|
|
181
|
+
jsonrpc: "2.0",
|
|
182
|
+
id,
|
|
183
|
+
method,
|
|
184
|
+
params: params ?? {},
|
|
185
|
+
};
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
runtime.pendingRequests.delete(id);
|
|
189
|
+
reject(new Error(`MCP request timed out: ${serverId} ${method} (${timeoutMs}ms)`));
|
|
190
|
+
}, timeoutMs);
|
|
191
|
+
runtime.pendingRequests.set(id, {
|
|
192
|
+
resolve: (value) => resolve(value),
|
|
193
|
+
reject,
|
|
194
|
+
timer,
|
|
195
|
+
});
|
|
196
|
+
this.writeFramedMessage(runtime, payload);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async initializeServer(serverId) {
|
|
200
|
+
// Primary protocol version + fallback for older servers
|
|
201
|
+
const initPayload = {
|
|
202
|
+
protocolVersion: "2024-11-05",
|
|
203
|
+
capabilities: {},
|
|
204
|
+
clientInfo: { name: "nano-pencil", version: "1.7.0" },
|
|
205
|
+
};
|
|
206
|
+
const fallbackPayload = {
|
|
207
|
+
protocolVersion: "2024-10-07",
|
|
208
|
+
capabilities: {},
|
|
209
|
+
clientInfo: { name: "nano-pencil", version: "1.7.0" },
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
await this.sendRequest(serverId, "initialize", initPayload, 20_000);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
await this.sendRequest(serverId, "initialize", fallbackPayload, 20_000);
|
|
216
|
+
}
|
|
217
|
+
this.sendNotification(serverId, "notifications/initialized");
|
|
218
|
+
}
|
|
219
|
+
normalizeToolRecord(serverId, tool) {
|
|
220
|
+
if (!tool || typeof tool !== "object")
|
|
221
|
+
return null;
|
|
222
|
+
const obj = tool;
|
|
223
|
+
const name = obj.name;
|
|
224
|
+
const description = obj.description;
|
|
225
|
+
if (typeof name !== "string" || typeof description !== "string") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const inputSchema = obj.inputSchema && typeof obj.inputSchema === "object"
|
|
229
|
+
? obj.inputSchema
|
|
230
|
+
: { type: "object", properties: {}, additionalProperties: true };
|
|
231
|
+
return {
|
|
232
|
+
name: `${serverId}/${name}`,
|
|
233
|
+
displayName: typeof obj.title === "string"
|
|
234
|
+
? obj.title
|
|
235
|
+
: typeof obj.displayName === "string"
|
|
236
|
+
? obj.displayName
|
|
237
|
+
: undefined,
|
|
238
|
+
description,
|
|
239
|
+
inputSchema,
|
|
240
|
+
serverId,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async loadToolsForServer(serverId) {
|
|
244
|
+
const tools = [];
|
|
245
|
+
let cursor;
|
|
246
|
+
while (true) {
|
|
247
|
+
const result = (await this.sendRequest(serverId, "tools/list", cursor ? { cursor } : {}, 20_000));
|
|
248
|
+
const pageTools = Array.isArray(result.tools) ? result.tools : [];
|
|
249
|
+
for (const t of pageTools) {
|
|
250
|
+
const normalized = this.normalizeToolRecord(serverId, t);
|
|
251
|
+
if (normalized)
|
|
252
|
+
tools.push(normalized);
|
|
253
|
+
}
|
|
254
|
+
const nextCursor = typeof result.nextCursor === "string"
|
|
255
|
+
? result.nextCursor
|
|
256
|
+
: typeof result.cursor === "string"
|
|
257
|
+
? result.cursor
|
|
258
|
+
: undefined;
|
|
259
|
+
if (!nextCursor || nextCursor === cursor)
|
|
260
|
+
break;
|
|
261
|
+
cursor = nextCursor;
|
|
262
|
+
}
|
|
263
|
+
this.serverTools.set(serverId, tools);
|
|
264
|
+
return tools;
|
|
265
|
+
}
|
|
70
266
|
/**
|
|
71
267
|
* Start an MCP server (for stdio transport)
|
|
72
268
|
*/
|
|
@@ -80,7 +276,7 @@ export class MCPClient {
|
|
|
80
276
|
return true;
|
|
81
277
|
}
|
|
82
278
|
// Check if already running
|
|
83
|
-
if (this.
|
|
279
|
+
if (this.serverRuntimes.has(serverId)) {
|
|
84
280
|
return true;
|
|
85
281
|
}
|
|
86
282
|
try {
|
|
@@ -88,13 +284,21 @@ export class MCPClient {
|
|
|
88
284
|
env: { ...process.env, ...server.env },
|
|
89
285
|
stdio: ["pipe", "pipe", "pipe"],
|
|
90
286
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
287
|
+
const runtime = {
|
|
288
|
+
process: serverProcess,
|
|
289
|
+
buffer: Buffer.alloc(0),
|
|
290
|
+
nextRequestId: 1,
|
|
291
|
+
pendingRequests: new Map(),
|
|
292
|
+
};
|
|
293
|
+
this.serverRuntimes.set(serverId, runtime);
|
|
294
|
+
this.attachStdoutParser(serverId, runtime);
|
|
295
|
+
await this.initializeServer(serverId);
|
|
296
|
+
await this.loadToolsForServer(serverId);
|
|
94
297
|
return true;
|
|
95
298
|
}
|
|
96
299
|
catch (error) {
|
|
97
300
|
console.error(`Failed to start MCP server ${serverId}:`, error);
|
|
301
|
+
this.stopServer(serverId);
|
|
98
302
|
return false;
|
|
99
303
|
}
|
|
100
304
|
}
|
|
@@ -102,17 +306,22 @@ export class MCPClient {
|
|
|
102
306
|
* Stop an MCP server
|
|
103
307
|
*/
|
|
104
308
|
stopServer(serverId) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
309
|
+
const runtime = this.serverRuntimes.get(serverId);
|
|
310
|
+
if (runtime) {
|
|
311
|
+
for (const pending of runtime.pendingRequests.values()) {
|
|
312
|
+
clearTimeout(pending.timer);
|
|
313
|
+
pending.reject(new Error(`MCP server ${serverId} stopped`));
|
|
314
|
+
}
|
|
315
|
+
runtime.pendingRequests.clear();
|
|
316
|
+
runtime.process.kill();
|
|
317
|
+
this.serverRuntimes.delete(serverId);
|
|
109
318
|
}
|
|
110
319
|
}
|
|
111
320
|
/**
|
|
112
321
|
* Stop all running servers
|
|
113
322
|
*/
|
|
114
323
|
stopAllServers() {
|
|
115
|
-
for (const serverId of this.
|
|
324
|
+
for (const serverId of this.serverRuntimes.keys()) {
|
|
116
325
|
this.stopServer(serverId);
|
|
117
326
|
}
|
|
118
327
|
}
|
|
@@ -122,12 +331,28 @@ export class MCPClient {
|
|
|
122
331
|
async listTools(serverId) {
|
|
123
332
|
const tools = [];
|
|
124
333
|
if (serverId) {
|
|
334
|
+
if (!this.serverTools.has(serverId)) {
|
|
335
|
+
try {
|
|
336
|
+
await this.loadToolsForServer(serverId);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// keep cache miss as empty list
|
|
340
|
+
}
|
|
341
|
+
}
|
|
125
342
|
const serverTools = this.serverTools.get(serverId) || [];
|
|
126
343
|
tools.push(...serverTools);
|
|
127
344
|
}
|
|
128
345
|
else {
|
|
129
|
-
for (const [
|
|
130
|
-
|
|
346
|
+
for (const [id] of this.servers.entries()) {
|
|
347
|
+
if (!this.serverTools.has(id)) {
|
|
348
|
+
try {
|
|
349
|
+
await this.loadToolsForServer(id);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// skip unavailable servers
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
tools.push(...(this.serverTools.get(id) || []));
|
|
131
356
|
}
|
|
132
357
|
}
|
|
133
358
|
return tools;
|
|
@@ -146,7 +371,7 @@ export class MCPClient {
|
|
|
146
371
|
error: `Server ${serverId} not found`,
|
|
147
372
|
};
|
|
148
373
|
}
|
|
149
|
-
// For SSE transport, make HTTP request
|
|
374
|
+
// For SSE transport, make HTTP request (not implemented in current defaults)
|
|
150
375
|
if (server.transport === "sse") {
|
|
151
376
|
return this.callSSETool(server, toolNameOnly, args);
|
|
152
377
|
}
|
|
@@ -157,34 +382,39 @@ export class MCPClient {
|
|
|
157
382
|
* Call tool via stdio (JSON-RPC)
|
|
158
383
|
*/
|
|
159
384
|
async callStdioTool(server, toolName, args) {
|
|
160
|
-
|
|
161
|
-
if (!process) {
|
|
385
|
+
if (!this.serverRuntimes.has(server.id)) {
|
|
162
386
|
return {
|
|
163
387
|
content: [{ type: "text", text: `Server ${server.id} is not running` }],
|
|
164
388
|
error: `Server ${server.id} is not running`,
|
|
165
389
|
};
|
|
166
390
|
}
|
|
167
391
|
try {
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
392
|
+
const result = (await this.sendRequest(server.id, "tools/call", { name: toolName, arguments: args }, 60_000));
|
|
393
|
+
const isError = result.isError === true;
|
|
394
|
+
const content = Array.isArray(result.content)
|
|
395
|
+
? result.content.map((item) => {
|
|
396
|
+
const type = item.type === "image" || item.type === "resource"
|
|
397
|
+
? item.type
|
|
398
|
+
: "text";
|
|
399
|
+
return {
|
|
400
|
+
type: type,
|
|
401
|
+
text: typeof item.text === "string"
|
|
402
|
+
? item.text
|
|
403
|
+
: typeof item.message === "string"
|
|
404
|
+
? item.message
|
|
405
|
+
: undefined,
|
|
406
|
+
data: item,
|
|
407
|
+
};
|
|
408
|
+
})
|
|
409
|
+
: [{ type: "text", text: JSON.stringify(result) }];
|
|
181
410
|
return {
|
|
182
|
-
content
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
411
|
+
content,
|
|
412
|
+
error: isError
|
|
413
|
+
? content
|
|
414
|
+
.map((c) => c.text)
|
|
415
|
+
.filter((t) => !!t)
|
|
416
|
+
.join("\n") || `MCP tool ${toolName} failed`
|
|
417
|
+
: undefined,
|
|
188
418
|
};
|
|
189
419
|
}
|
|
190
420
|
catch (error) {
|
|
@@ -200,7 +430,12 @@ export class MCPClient {
|
|
|
200
430
|
async callSSETool(server, toolName, args) {
|
|
201
431
|
// TODO: Implement SSE tool calls
|
|
202
432
|
return {
|
|
203
|
-
content: [
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "text",
|
|
436
|
+
text: `SSE transport is not implemented yet for ${server.id}/${toolName}`,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
204
439
|
error: "SSE transport not yet supported",
|
|
205
440
|
};
|
|
206
441
|
}
|
package/dist/core/mcp-manager.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Manages MCP client lifecycle and tool integration.
|
|
5
5
|
*/
|
|
6
6
|
import { MCPClient } from "./mcp/mcp-client.js";
|
|
7
|
+
import { loadMCPTools } from "./mcp/mcp-adapter.js";
|
|
7
8
|
import { listEnabledMCPServers } from "./mcp/mcp-config.js";
|
|
8
9
|
export class MCPManager {
|
|
9
10
|
client;
|
|
@@ -25,8 +26,7 @@ export class MCPManager {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
// Load tools from all servers
|
|
28
|
-
|
|
29
|
-
// this.tools = await loadMCPTools(this.client);
|
|
29
|
+
this.tools = await loadMCPTools(this.client);
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Get all MCP tools as NanoPencil ToolDefinitions
|
package/dist/core/sdk.js
CHANGED
|
@@ -234,15 +234,14 @@ export async function createAgentSession(options = {}) {
|
|
|
234
234
|
}
|
|
235
235
|
// Initialize MCP if enabled (before creating AgentSession)
|
|
236
236
|
let mcpManager;
|
|
237
|
-
|
|
238
|
-
// Will be re-enabled after refactoring tool adapter
|
|
239
|
-
// const mcpTools: ToolDefinition[] = [];
|
|
237
|
+
const mcpTools = [];
|
|
240
238
|
if (options.enableMCP) {
|
|
241
239
|
try {
|
|
242
240
|
mcpManager = new MCPManager();
|
|
243
241
|
await mcpManager.initialize();
|
|
244
|
-
|
|
242
|
+
mcpTools.push(...mcpManager.getTools());
|
|
245
243
|
time("mcp.initialize");
|
|
244
|
+
process.once("exit", () => mcpManager?.dispose());
|
|
246
245
|
}
|
|
247
246
|
catch (error) {
|
|
248
247
|
console.warn(`Failed to initialize MCP: ${error}`);
|
|
@@ -273,7 +272,7 @@ export async function createAgentSession(options = {}) {
|
|
|
273
272
|
cwd,
|
|
274
273
|
scopedModels: options.scopedModels,
|
|
275
274
|
resourceLoader,
|
|
276
|
-
customTools: options.customTools,
|
|
275
|
+
customTools: [...mcpTools, ...(options.customTools ?? [])],
|
|
277
276
|
modelRegistry,
|
|
278
277
|
initialActiveToolNames,
|
|
279
278
|
extensionRunnerRef,
|
package/dist/main.js
CHANGED
|
@@ -617,6 +617,8 @@ export async function main(args) {
|
|
|
617
617
|
sessionManager = SessionManager.open(selectedPath);
|
|
618
618
|
}
|
|
619
619
|
const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager);
|
|
620
|
+
// NanoPencil 默认启用 MCP;离线模式下关闭以避免启动阻塞。
|
|
621
|
+
sessionOptions.enableMCP = APP_NAME === "nanopencil" && !offlineMode;
|
|
620
622
|
sessionOptions.authStorage = authStorage;
|
|
621
623
|
sessionOptions.modelRegistry = modelRegistry;
|
|
622
624
|
sessionOptions.resourceLoader = resourceLoader;
|
|
@@ -112,6 +112,8 @@ export class InteractiveMode {
|
|
|
112
112
|
retryEscapeHandler;
|
|
113
113
|
// Messages queued while compaction is running
|
|
114
114
|
compactionQueuedMessages = [];
|
|
115
|
+
// User messages rendered optimistically before Agent emits message_start
|
|
116
|
+
optimisticUserMessages = [];
|
|
115
117
|
// Shutdown state
|
|
116
118
|
shutdownRequested = false;
|
|
117
119
|
// Extension UI state
|
|
@@ -1710,10 +1712,24 @@ export class InteractiveMode {
|
|
|
1710
1712
|
}
|
|
1711
1713
|
this.editor.addToHistory?.(text);
|
|
1712
1714
|
this.editor.setText("");
|
|
1715
|
+
if (!text.startsWith("/")) {
|
|
1716
|
+
this.optimisticUserMessages.push(text);
|
|
1717
|
+
this.addMessageToChat({
|
|
1718
|
+
role: "user",
|
|
1719
|
+
content: [{ type: "text", text }],
|
|
1720
|
+
timestamp: Date.now(),
|
|
1721
|
+
});
|
|
1722
|
+
this.ui.requestRender();
|
|
1723
|
+
}
|
|
1713
1724
|
try {
|
|
1714
1725
|
await this.session.prompt(text);
|
|
1715
1726
|
}
|
|
1716
1727
|
catch (error) {
|
|
1728
|
+
if (!text.startsWith("/") &&
|
|
1729
|
+
this.optimisticUserMessages.length > 0 &&
|
|
1730
|
+
this.optimisticUserMessages[0] === text) {
|
|
1731
|
+
this.optimisticUserMessages.shift();
|
|
1732
|
+
}
|
|
1717
1733
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1718
1734
|
this.showError(errorMessage);
|
|
1719
1735
|
}
|
|
@@ -1764,6 +1780,15 @@ export class InteractiveMode {
|
|
|
1764
1780
|
this.ui.requestRender();
|
|
1765
1781
|
}
|
|
1766
1782
|
else if (event.message.role === "user") {
|
|
1783
|
+
const userText = this.getUserMessageText(event.message);
|
|
1784
|
+
if (userText &&
|
|
1785
|
+
this.optimisticUserMessages.length > 0 &&
|
|
1786
|
+
this.optimisticUserMessages[0] === userText) {
|
|
1787
|
+
this.optimisticUserMessages.shift();
|
|
1788
|
+
this.updatePendingMessagesDisplay();
|
|
1789
|
+
this.ui.requestRender();
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1767
1792
|
this.addMessageToChat(event.message);
|
|
1768
1793
|
this.updatePendingMessagesDisplay();
|
|
1769
1794
|
this.ui.requestRender();
|