@pencil-agent/nano-pencil 1.1.0 → 1.2.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/README.md CHANGED
@@ -24,6 +24,16 @@ nanopencil
24
24
 
25
25
  进入后可直接输入需求,模型会使用 read、write、edit、bash 等工具完成任务。
26
26
 
27
+ ### MCP 支持
28
+
29
+ NanoPencil 内置支持 MCP (Model Context Protocol),**默认启用**免费工具(Filesystem、Fetch、Puppeteer、SQLite、Git)。
30
+
31
+ ```bash
32
+ nanopencil # MCP 默认启用
33
+ ```
34
+
35
+ 详见 [MCP 文档](docs/MCP_GUIDE.md) 和 [内置工具列表](docs/BUILTIN_MCP_TOOLS.md)。
36
+
27
37
  ## 常用命令与快捷键
28
38
 
29
39
  在输入框输入 `/` 可触发命令,例如:
package/core/index.ts CHANGED
@@ -1,61 +1,87 @@
1
- /**
2
- * Core modules shared between all run modes.
3
- */
4
-
5
- export {
6
- AgentSession,
7
- type AgentSessionConfig,
8
- type AgentSessionEvent,
9
- type AgentSessionEventListener,
10
- type ModelCycleResult,
11
- type PromptOptions,
12
- type SessionStats,
13
- } from "./agent-session.js";
14
- export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations } from "./bash-executor.js";
15
- export type { CompactionResult } from "./compaction/index.js";
16
- export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js";
17
-
18
- // Extensions system
19
- export {
20
- type AgentEndEvent,
21
- type AgentStartEvent,
22
- type AgentToolResult,
23
- type AgentToolUpdateCallback,
24
- type BeforeAgentStartEvent,
25
- type ContextEvent,
26
- discoverAndLoadExtensions,
27
- type ExecOptions,
28
- type ExecResult,
29
- type Extension,
30
- type ExtensionAPI,
31
- type ExtensionCommandContext,
32
- type ExtensionContext,
33
- type ExtensionError,
34
- type ExtensionEvent,
35
- type ExtensionFactory,
36
- type ExtensionFlag,
37
- type ExtensionHandler,
38
- ExtensionRunner,
39
- type ExtensionShortcut,
40
- type ExtensionUIContext,
41
- type LoadExtensionsResult,
42
- type MessageRenderer,
43
- type RegisteredCommand,
44
- type SessionBeforeCompactEvent,
45
- type SessionBeforeForkEvent,
46
- type SessionBeforeSwitchEvent,
47
- type SessionBeforeTreeEvent,
48
- type SessionCompactEvent,
49
- type SessionForkEvent,
50
- type SessionShutdownEvent,
51
- type SessionStartEvent,
52
- type SessionSwitchEvent,
53
- type SessionTreeEvent,
54
- type ToolCallEvent,
55
- type ToolDefinition,
56
- type ToolRenderResultOptions,
57
- type ToolResultEvent,
58
- type TurnEndEvent,
59
- type TurnStartEvent,
60
- wrapToolsWithExtensions,
61
- } from "./extensions/index.js";
1
+ /**
2
+ * Core modules shared between all run modes.
3
+ */
4
+
5
+ export {
6
+ AgentSession,
7
+ type AgentSessionConfig,
8
+ type AgentSessionEvent,
9
+ type AgentSessionEventListener,
10
+ type ModelCycleResult,
11
+ type PromptOptions,
12
+ type SessionStats,
13
+ } from "./agent-session.js";
14
+ export {
15
+ type BashExecutorOptions,
16
+ type BashResult,
17
+ executeBash,
18
+ executeBashWithOperations,
19
+ } from "./bash-executor.js";
20
+ export type { CompactionResult } from "./compaction/index.js";
21
+ export {
22
+ createEventBus,
23
+ type EventBus,
24
+ type EventBusController,
25
+ } from "./event-bus.js";
26
+
27
+ // Extensions system
28
+ export {
29
+ type AgentEndEvent,
30
+ type AgentStartEvent,
31
+ type AgentToolResult,
32
+ type AgentToolUpdateCallback,
33
+ type BeforeAgentStartEvent,
34
+ type ContextEvent,
35
+ discoverAndLoadExtensions,
36
+ type ExecOptions,
37
+ type ExecResult,
38
+ type Extension,
39
+ type ExtensionAPI,
40
+ type ExtensionCommandContext,
41
+ type ExtensionContext,
42
+ type ExtensionError,
43
+ type ExtensionEvent,
44
+ type ExtensionFactory,
45
+ type ExtensionFlag,
46
+ type ExtensionHandler,
47
+ ExtensionRunner,
48
+ type ExtensionShortcut,
49
+ type ExtensionUIContext,
50
+ type LoadExtensionsResult,
51
+ type MessageRenderer,
52
+ type RegisteredCommand,
53
+ type SessionBeforeCompactEvent,
54
+ type SessionBeforeForkEvent,
55
+ type SessionBeforeSwitchEvent,
56
+ type SessionBeforeTreeEvent,
57
+ type SessionCompactEvent,
58
+ type SessionForkEvent,
59
+ type SessionShutdownEvent,
60
+ type SessionStartEvent,
61
+ type SessionSwitchEvent,
62
+ type SessionTreeEvent,
63
+ type ToolCallEvent,
64
+ type ToolDefinition,
65
+ type ToolRenderResultOptions,
66
+ type ToolResultEvent,
67
+ type TurnEndEvent,
68
+ type TurnStartEvent,
69
+ wrapToolsWithExtensions,
70
+ } from "./extensions/index.js";
71
+
72
+ // MCP (Model Context Protocol) support
73
+ export { MCPManager } from "./mcp-manager.js";
74
+ export type {
75
+ MCPServerConfig,
76
+ MCPTool,
77
+ MCPToolResult,
78
+ } from "./mcp/mcp-client.js";
79
+ export {
80
+ loadMCPConfig,
81
+ saveMCPConfig,
82
+ addMCPServer,
83
+ removeMCPServer,
84
+ getMCPServer,
85
+ listMCPServers,
86
+ listEnabledMCPServers,
87
+ } from "./mcp/mcp-config.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Module
3
+ *
4
+ * Exports MCP client and adapter functionality.
5
+ */
6
+
7
+ export { MCPClient } from "./mcp-client.js";
8
+ export type { MCPServerConfig, MCPTool, MCPToolResult } from "./mcp-client.js";
9
+
10
+ export {
11
+ createMCPTool,
12
+ getMCPToolDisplayName,
13
+ loadMCPTools,
14
+ } from "./mcp-adapter.js";
15
+ export {
16
+ API_KEY_GUIDANCE,
17
+ formatGuidanceMessage,
18
+ getAPIKeyGuidance,
19
+ getMissingKeyServers,
20
+ getOptionalAPIKeyServers,
21
+ requiresAPIKey,
22
+ } from "./mcp-guidance.js";
23
+ export type { APIKeyGuidance } from "./mcp-guidance.js";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * MCP Tool Adapter
3
+ *
4
+ * Adapts MCP tools to work with NanoPencil's tool system.
5
+ */
6
+
7
+ import type { ToolDefinition } from "../extensions/index.js";
8
+ import type { MCPClient, MCPTool } from "./mcp-client.js";
9
+ import { formatGuidanceMessage, getAPIKeyGuidance } from "./mcp-guidance.js";
10
+
11
+ /**
12
+ * Create a NanoPencil ToolDefinition from an MCP tool definition
13
+ */
14
+ export function createMCPTool(
15
+ mcpClient: MCPClient,
16
+ mcpTool: MCPTool,
17
+ ): ToolDefinition {
18
+ const toolName = mcpTool.name; // Full name like "filesystem/read"
19
+ const [serverId] = toolName.split("/");
20
+
21
+ return {
22
+ name: toolName,
23
+ label: toolName,
24
+ description: mcpTool.description,
25
+ // Use TypeBox Object schema with any properties since MCP tools have dynamic schemas
26
+ parameters: {
27
+ type: "object",
28
+ properties: {},
29
+ additionalProperties: true,
30
+ } as any,
31
+
32
+ async execute(
33
+ toolCallId: string,
34
+ params: Record<string, any>,
35
+ signal: AbortSignal | undefined,
36
+ onUpdate: ((details: any) => void) | undefined,
37
+ ctx: any,
38
+ ) {
39
+ try {
40
+ const result = await mcpClient.callTool(toolName, params);
41
+
42
+ if (result.error) {
43
+ // Check if error is due to missing API key and provide guidance
44
+ const guidance = getAPIKeyGuidance(serverId);
45
+ if (guidance && result.error?.toLowerCase().includes("key")) {
46
+ return {
47
+ content: [
48
+ { type: "text", text: formatGuidanceMessage(guidance, true) },
49
+ ],
50
+ details: { error: result.error },
51
+ };
52
+ }
53
+
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: result.content.map((c) => c.text || "").join("\n"),
59
+ },
60
+ ],
61
+ details: { error: result.error },
62
+ };
63
+ }
64
+
65
+ // Format tool result for NanoPencil
66
+ const output = result.content
67
+ .map((c) => {
68
+ if (c.type === "text") {
69
+ return c.text || "";
70
+ } else if (c.type === "image") {
71
+ return `[Image: ${c.data?.uri || "unknown"}]`;
72
+ } else if (c.type === "resource") {
73
+ return `[Resource: ${JSON.stringify(c.data)}]`;
74
+ }
75
+ return "";
76
+ })
77
+ .filter(Boolean)
78
+ .join("\n");
79
+
80
+ return {
81
+ content: [{ type: "text", text: output }],
82
+ details: undefined,
83
+ };
84
+ } catch (error) {
85
+ // Check if error is related to missing API key and provide guidance
86
+ const guidance = getAPIKeyGuidance(serverId);
87
+ if (guidance && String(error).toLowerCase().includes("key")) {
88
+ return {
89
+ content: [
90
+ { type: "text", text: formatGuidanceMessage(guidance, true) },
91
+ ],
92
+ details: {
93
+ error: error instanceof Error ? error.message : String(error),
94
+ },
95
+ };
96
+ }
97
+
98
+ return {
99
+ content: [
100
+ { type: "text", text: `Failed to call MCP tool ${toolName}` },
101
+ ],
102
+ details: {
103
+ error: error instanceof Error ? error.message : String(error),
104
+ },
105
+ };
106
+ }
107
+ },
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Load all MCP tools as NanoPencil ToolDefinitions
113
+ */
114
+ export async function loadMCPTools(
115
+ mcpClient: MCPClient,
116
+ ): Promise<ToolDefinition[]> {
117
+ const mcpTools = await mcpClient.listTools();
118
+
119
+ return mcpTools.map((mcpTool) => createMCPTool(mcpClient, mcpTool));
120
+ }
121
+
122
+ /**
123
+ * Get a human-readable display name for an MCP tool
124
+ */
125
+ export function getMCPToolDisplayName(mcpTool: MCPTool): string {
126
+ if (mcpTool.displayName) {
127
+ return mcpTool.displayName;
128
+ }
129
+
130
+ const [serverId, ...nameParts] = mcpTool.name.split("/");
131
+ const toolName = nameParts.join("/");
132
+
133
+ return `${serverId}/${toolName}`;
134
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Client
3
+ *
4
+ * Provides client functionality to connect to MCP servers and call their tools.
5
+ * Supports both stdio and SSE transport types.
6
+ */
7
+
8
+ import { spawn } from "child_process";
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { join } from "path";
11
+ import { getAgentDir } from "../../config.js";
12
+
13
+ export interface MCPServerConfig {
14
+ /** Unique identifier for this server */
15
+ id: string;
16
+ /** Display name */
17
+ name: string;
18
+ /** Command to start the server (e.g., "npx", "uvx") */
19
+ command: string;
20
+ /** Arguments to pass to the command */
21
+ args: string[];
22
+ /** Environment variables to pass */
23
+ env?: Record<string, string>;
24
+ /** Transport type: "stdio" or "sse" */
25
+ transport?: "stdio" | "sse";
26
+ /** Whether this server is enabled */
27
+ enabled?: boolean;
28
+ }
29
+
30
+ export interface MCPTool {
31
+ /** Tool name (server_id/tool_name format) */
32
+ name: string;
33
+ /** Display name */
34
+ displayName?: string;
35
+ /** Tool description */
36
+ description: string;
37
+ /** JSON Schema for input */
38
+ inputSchema: any;
39
+ /** Server ID */
40
+ serverId: string;
41
+ }
42
+
43
+ export interface MCPToolResult {
44
+ /** Tool result content */
45
+ content: Array<{
46
+ type: "text" | "image" | "resource";
47
+ text?: string;
48
+ data?: any;
49
+ }>;
50
+ /** Error message if call failed */
51
+ error?: string;
52
+ /** Whether result is partial (hasMore=true) */
53
+ isPartial?: boolean;
54
+ }
55
+
56
+ /**
57
+ * MCP Client class
58
+ * Manages connections to MCP servers and tool calls
59
+ */
60
+ export class MCPClient {
61
+ private servers = new Map<string, MCPServerConfig>();
62
+ private serverProcesses = new Map<string, any>();
63
+ private serverTools = new Map<string, MCPTool[]>();
64
+
65
+ constructor() {
66
+ this.loadServersFromConfig();
67
+ }
68
+
69
+ /**
70
+ * Load MCP server configurations from config file
71
+ */
72
+ private loadServersFromConfig(): void {
73
+ const configDir = getAgentDir();
74
+ const configPath = join(configDir, "mcp.json");
75
+
76
+ if (!existsSync(configPath)) {
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
82
+ const servers: MCPServerConfig[] = config.mcpServers || [];
83
+
84
+ for (const server of servers) {
85
+ if (server.enabled !== false) {
86
+ this.servers.set(server.id, server);
87
+ }
88
+ }
89
+ } catch (error) {
90
+ console.error(`Failed to load MCP config: ${error}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get all configured servers
96
+ */
97
+ getServers(): MCPServerConfig[] {
98
+ return Array.from(this.servers.values());
99
+ }
100
+
101
+ /**
102
+ * Get a specific server by ID
103
+ */
104
+ getServer(id: string): MCPServerConfig | undefined {
105
+ return this.servers.get(id);
106
+ }
107
+
108
+ /**
109
+ * Add or update a server configuration
110
+ */
111
+ addServer(server: MCPServerConfig): void {
112
+ this.servers.set(server.id, server);
113
+ }
114
+
115
+ /**
116
+ * Remove a server
117
+ */
118
+ removeServer(id: string): void {
119
+ this.servers.delete(id);
120
+ this.serverTools.delete(id);
121
+ this.stopServer(id);
122
+ }
123
+
124
+ /**
125
+ * Start an MCP server (for stdio transport)
126
+ */
127
+ async startServer(serverId: string): Promise<boolean> {
128
+ const server = this.servers.get(serverId);
129
+ if (!server) {
130
+ throw new Error(`Server ${serverId} not found`);
131
+ }
132
+
133
+ if (server.transport === "sse") {
134
+ // SSE servers don't need to be started as separate processes
135
+ return true;
136
+ }
137
+
138
+ // Check if already running
139
+ if (this.serverProcesses.has(serverId)) {
140
+ return true;
141
+ }
142
+
143
+ try {
144
+ const serverProcess = spawn(server.command, server.args, {
145
+ env: { ...process.env, ...server.env },
146
+ stdio: ["pipe", "pipe", "pipe"],
147
+ });
148
+
149
+ this.serverProcesses.set(serverId, serverProcess);
150
+
151
+ // TODO: Initialize MCP handshake, list tools
152
+ // For now, we'll mark it as started
153
+ return true;
154
+ } catch (error) {
155
+ console.error(`Failed to start MCP server ${serverId}:`, error);
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Stop an MCP server
162
+ */
163
+ stopServer(serverId: string): void {
164
+ const process = this.serverProcesses.get(serverId);
165
+ if (process) {
166
+ process.kill();
167
+ this.serverProcesses.delete(serverId);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Stop all running servers
173
+ */
174
+ stopAllServers(): void {
175
+ for (const serverId of this.serverProcesses.keys()) {
176
+ this.stopServer(serverId);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * List available tools from all servers
182
+ */
183
+ async listTools(serverId?: string): Promise<MCPTool[]> {
184
+ const tools: MCPTool[] = [];
185
+
186
+ if (serverId) {
187
+ const serverTools = this.serverTools.get(serverId) || [];
188
+ tools.push(...serverTools);
189
+ } else {
190
+ for (const [_id, serverTools] of this.serverTools) {
191
+ tools.push(...serverTools);
192
+ }
193
+ }
194
+
195
+ return tools;
196
+ }
197
+
198
+ /**
199
+ * Call an MCP tool
200
+ */
201
+ async callTool(
202
+ toolName: string,
203
+ args: Record<string, any>,
204
+ ): Promise<MCPToolResult> {
205
+ // Parse tool name: server_id/tool_name
206
+ const [serverId, ...nameParts] = toolName.split("/");
207
+ const toolNameOnly = nameParts.join("/");
208
+
209
+ const server = this.servers.get(serverId);
210
+ if (!server) {
211
+ return {
212
+ content: [{ type: "text", text: `Server ${serverId} not found` }],
213
+ error: `Server ${serverId} not found`,
214
+ };
215
+ }
216
+
217
+ // For SSE transport, make HTTP request
218
+ if (server.transport === "sse") {
219
+ return this.callSSETool(server, toolNameOnly, args);
220
+ }
221
+
222
+ // For stdio transport, send JSON-RPC message
223
+ return this.callStdioTool(server, toolNameOnly, args);
224
+ }
225
+
226
+ /**
227
+ * Call tool via stdio (JSON-RPC)
228
+ */
229
+ private async callStdioTool(
230
+ server: MCPServerConfig,
231
+ toolName: string,
232
+ args: Record<string, any>,
233
+ ): Promise<MCPToolResult> {
234
+ const process = this.serverProcesses.get(server.id);
235
+ if (!process) {
236
+ return {
237
+ content: [{ type: "text", text: `Server ${server.id} is not running` }],
238
+ error: `Server ${server.id} is not running`,
239
+ };
240
+ }
241
+
242
+ try {
243
+ // Send JSON-RPC request
244
+ const request = {
245
+ jsonrpc: "2.0",
246
+ id: Date.now(),
247
+ method: "tools/call",
248
+ params: {
249
+ name: toolName,
250
+ arguments: args,
251
+ },
252
+ };
253
+
254
+ process.stdin.write(JSON.stringify(request) + "\n");
255
+
256
+ // Read response (simplified - in production, use proper message handling)
257
+ // For now, return a placeholder
258
+ return {
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: `Tool ${toolName} called (TODO: implement response handling)`,
263
+ },
264
+ ],
265
+ };
266
+ } catch (error) {
267
+ return {
268
+ content: [{ type: "text", text: `Failed to call tool: ${error}` }],
269
+ error: String(error),
270
+ };
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Call tool via SSE (HTTP)
276
+ */
277
+ private async callSSETool(
278
+ server: MCPServerConfig,
279
+ toolName: string,
280
+ args: Record<string, any>,
281
+ ): Promise<MCPToolResult> {
282
+ // TODO: Implement SSE tool calls
283
+ return {
284
+ content: [{ type: "text", text: `SSE tool calls not yet implemented` }],
285
+ error: "SSE transport not yet supported",
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Check if a tool exists
291
+ */
292
+ hasTool(toolName: string): boolean {
293
+ const [serverId] = toolName.split("/");
294
+ return this.servers.has(serverId);
295
+ }
296
+ }