@oh-my-pi/pi-coding-agent 1.337.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/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP to CustomTool bridge.
|
|
3
|
+
*
|
|
4
|
+
* Converts MCP tool definitions to CustomTool format for the agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
8
|
+
import type { CustomTool, CustomToolResult } from "../custom-tools/types.js";
|
|
9
|
+
import { callTool } from "./client.js";
|
|
10
|
+
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** Details included in MCP tool results for rendering */
|
|
13
|
+
export interface MCPToolDetails {
|
|
14
|
+
/** Server name */
|
|
15
|
+
serverName: string;
|
|
16
|
+
/** Original MCP tool name */
|
|
17
|
+
mcpToolName: string;
|
|
18
|
+
/** Whether the call resulted in an error */
|
|
19
|
+
isError?: boolean;
|
|
20
|
+
/** Raw content from MCP response */
|
|
21
|
+
rawContent?: MCPContent[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert JSON Schema from MCP to TypeBox-compatible schema.
|
|
26
|
+
* MCP uses standard JSON Schema, TypeBox uses a compatible subset.
|
|
27
|
+
*/
|
|
28
|
+
function convertSchema(mcpSchema: MCPToolDefinition["inputSchema"]): TSchema {
|
|
29
|
+
// MCP schemas are JSON Schema objects, TypeBox can use them directly
|
|
30
|
+
// as long as we ensure the structure is correct
|
|
31
|
+
return mcpSchema as unknown as TSchema;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format MCP content for LLM consumption.
|
|
36
|
+
*/
|
|
37
|
+
function formatMCPContent(content: MCPContent[]): string {
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const item of content) {
|
|
41
|
+
switch (item.type) {
|
|
42
|
+
case "text":
|
|
43
|
+
parts.push(item.text);
|
|
44
|
+
break;
|
|
45
|
+
case "image":
|
|
46
|
+
parts.push(`[Image: ${item.mimeType}]`);
|
|
47
|
+
break;
|
|
48
|
+
case "resource":
|
|
49
|
+
if (item.resource.text) {
|
|
50
|
+
parts.push(`[Resource: ${item.resource.uri}]\n${item.resource.text}`);
|
|
51
|
+
} else {
|
|
52
|
+
parts.push(`[Resource: ${item.resource.uri}]`);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return parts.join("\n\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a unique tool name for an MCP tool.
|
|
63
|
+
* Prefixes with server name to avoid conflicts.
|
|
64
|
+
*/
|
|
65
|
+
export function createMCPToolName(serverName: string, toolName: string): string {
|
|
66
|
+
// Use underscore separator since tool names can't have special chars
|
|
67
|
+
return `mcp_${serverName}_${toolName}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse an MCP tool name back to server and tool components.
|
|
72
|
+
*/
|
|
73
|
+
export function parseMCPToolName(name: string): { serverName: string; toolName: string } | null {
|
|
74
|
+
if (!name.startsWith("mcp_")) return null;
|
|
75
|
+
|
|
76
|
+
const rest = name.slice(4);
|
|
77
|
+
const underscoreIdx = rest.indexOf("_");
|
|
78
|
+
if (underscoreIdx === -1) return null;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
serverName: rest.slice(0, underscoreIdx),
|
|
82
|
+
toolName: rest.slice(underscoreIdx + 1),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert an MCP tool definition to a CustomTool.
|
|
88
|
+
*/
|
|
89
|
+
export function createMCPTool(
|
|
90
|
+
connection: MCPServerConnection,
|
|
91
|
+
tool: MCPToolDefinition,
|
|
92
|
+
): CustomTool<TSchema, MCPToolDetails> {
|
|
93
|
+
const name = createMCPToolName(connection.name, tool.name);
|
|
94
|
+
const schema = convertSchema(tool.inputSchema);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name,
|
|
98
|
+
label: `${connection.name}/${tool.name}`,
|
|
99
|
+
description: tool.description ?? `MCP tool from ${connection.name}`,
|
|
100
|
+
parameters: schema,
|
|
101
|
+
|
|
102
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, _signal): Promise<CustomToolResult<MCPToolDetails>> {
|
|
103
|
+
try {
|
|
104
|
+
const result = await callTool(connection, tool.name, params as Record<string, unknown>);
|
|
105
|
+
|
|
106
|
+
const text = formatMCPContent(result.content);
|
|
107
|
+
const details: MCPToolDetails = {
|
|
108
|
+
serverName: connection.name,
|
|
109
|
+
mcpToolName: tool.name,
|
|
110
|
+
isError: result.isError,
|
|
111
|
+
rawContent: result.content,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (result.isError) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: `Error: ${text}` }],
|
|
117
|
+
details,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text }],
|
|
123
|
+
details,
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
129
|
+
details: {
|
|
130
|
+
serverName: connection.name,
|
|
131
|
+
mcpToolName: tool.name,
|
|
132
|
+
isError: true,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convert all tools from an MCP server to CustomTools.
|
|
142
|
+
*/
|
|
143
|
+
export function createMCPTools(
|
|
144
|
+
connection: MCPServerConnection,
|
|
145
|
+
tools: MCPToolDefinition[],
|
|
146
|
+
): CustomTool<TSchema, MCPToolDetails>[] {
|
|
147
|
+
return tools.map((tool) => createMCPTool(connection, tool));
|
|
148
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP transport (Streamable HTTP).
|
|
3
|
+
*
|
|
4
|
+
* Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
|
|
5
|
+
* Based on MCP spec 2025-03-26.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JsonRpcResponse, MCPHttpServerConfig, MCPSseServerConfig, MCPTransport } from "../types.js";
|
|
9
|
+
|
|
10
|
+
/** Generate unique request ID */
|
|
11
|
+
function generateId(): string {
|
|
12
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse SSE data line */
|
|
16
|
+
function parseSSELine(line: string): { event?: string; data?: string; id?: string } | null {
|
|
17
|
+
if (line.startsWith("data:")) {
|
|
18
|
+
return { data: line.slice(5).trim() };
|
|
19
|
+
}
|
|
20
|
+
if (line.startsWith("event:")) {
|
|
21
|
+
return { event: line.slice(6).trim() };
|
|
22
|
+
}
|
|
23
|
+
if (line.startsWith("id:")) {
|
|
24
|
+
return { id: line.slice(3).trim() };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* HTTP transport for MCP servers.
|
|
31
|
+
* Uses POST for requests, supports SSE responses.
|
|
32
|
+
*/
|
|
33
|
+
export class HttpTransport implements MCPTransport {
|
|
34
|
+
private _connected = false;
|
|
35
|
+
private sessionId: string | null = null;
|
|
36
|
+
private sseConnection: AbortController | null = null;
|
|
37
|
+
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
onError?: (error: Error) => void;
|
|
40
|
+
onNotification?: (method: string, params: unknown) => void;
|
|
41
|
+
|
|
42
|
+
constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
|
|
43
|
+
|
|
44
|
+
get connected(): boolean {
|
|
45
|
+
return this._connected;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get url(): string {
|
|
49
|
+
return this.config.url;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mark transport as connected.
|
|
54
|
+
* HTTP doesn't need persistent connection, but we track state.
|
|
55
|
+
*/
|
|
56
|
+
async connect(): Promise<void> {
|
|
57
|
+
if (this._connected) return;
|
|
58
|
+
this._connected = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start SSE listener for server-initiated messages.
|
|
63
|
+
* Optional - only needed if server sends notifications.
|
|
64
|
+
*/
|
|
65
|
+
async startSSEListener(): Promise<void> {
|
|
66
|
+
if (!this._connected) return;
|
|
67
|
+
if (this.sseConnection) return;
|
|
68
|
+
|
|
69
|
+
this.sseConnection = new AbortController();
|
|
70
|
+
const headers: Record<string, string> = {
|
|
71
|
+
Accept: "text/event-stream",
|
|
72
|
+
...this.config.headers,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (this.sessionId) {
|
|
76
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(this.config.url, {
|
|
81
|
+
method: "GET",
|
|
82
|
+
headers,
|
|
83
|
+
signal: this.sseConnection.signal,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (response.status === 405) {
|
|
87
|
+
// Server doesn't support SSE listening, that's OK
|
|
88
|
+
this.sseConnection = null;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!response.ok || !response.body) {
|
|
93
|
+
this.sseConnection = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Read SSE stream
|
|
98
|
+
const reader = response.body.getReader();
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
let buffer = "";
|
|
101
|
+
|
|
102
|
+
while (this._connected) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
|
|
106
|
+
buffer += decoder.decode(value, { stream: true });
|
|
107
|
+
const lines = buffer.split("\n");
|
|
108
|
+
buffer = lines.pop() ?? "";
|
|
109
|
+
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
const parsed = parseSSELine(line);
|
|
112
|
+
if (parsed?.data && parsed.data !== "[DONE]") {
|
|
113
|
+
try {
|
|
114
|
+
const message = JSON.parse(parsed.data);
|
|
115
|
+
if ("method" in message && !("id" in message)) {
|
|
116
|
+
this.onNotification?.(message.method, message.params);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore parse errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
126
|
+
this.onError?.(error);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
this.sseConnection = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
134
|
+
if (!this._connected) {
|
|
135
|
+
throw new Error("Transport not connected");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const id = generateId();
|
|
139
|
+
const body = {
|
|
140
|
+
jsonrpc: "2.0" as const,
|
|
141
|
+
id,
|
|
142
|
+
method,
|
|
143
|
+
params: params ?? {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const headers: Record<string, string> = {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
Accept: "application/json, text/event-stream",
|
|
149
|
+
...this.config.headers,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (this.sessionId) {
|
|
153
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const response = await fetch(this.config.url, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers,
|
|
159
|
+
body: JSON.stringify(body),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Check for session ID in response
|
|
163
|
+
const newSessionId = response.headers.get("Mcp-Session-Id");
|
|
164
|
+
if (newSessionId) {
|
|
165
|
+
this.sessionId = newSessionId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const text = await response.text();
|
|
170
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
174
|
+
|
|
175
|
+
// Handle SSE response
|
|
176
|
+
if (contentType.includes("text/event-stream")) {
|
|
177
|
+
return this.parseSSEResponse<T>(response, id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle JSON response
|
|
181
|
+
const result = (await response.json()) as JsonRpcResponse;
|
|
182
|
+
|
|
183
|
+
if (result.error) {
|
|
184
|
+
throw new Error(`MCP error ${result.error.code}: ${result.error.message}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result.result as T;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async parseSSEResponse<T>(response: Response, expectedId: string | number): Promise<T> {
|
|
191
|
+
if (!response.body) {
|
|
192
|
+
throw new Error("No response body");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const reader = response.body.getReader();
|
|
196
|
+
const decoder = new TextDecoder();
|
|
197
|
+
let buffer = "";
|
|
198
|
+
let result: T | undefined;
|
|
199
|
+
|
|
200
|
+
while (true) {
|
|
201
|
+
const { done, value } = await reader.read();
|
|
202
|
+
if (done) break;
|
|
203
|
+
|
|
204
|
+
buffer += decoder.decode(value, { stream: true });
|
|
205
|
+
const lines = buffer.split("\n");
|
|
206
|
+
buffer = lines.pop() ?? "";
|
|
207
|
+
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const parsed = parseSSELine(line);
|
|
210
|
+
if (parsed?.data && parsed.data !== "[DONE]") {
|
|
211
|
+
try {
|
|
212
|
+
const message = JSON.parse(parsed.data) as JsonRpcResponse;
|
|
213
|
+
|
|
214
|
+
// Handle our response
|
|
215
|
+
if ("id" in message && message.id === expectedId) {
|
|
216
|
+
if (message.error) {
|
|
217
|
+
throw new Error(`MCP error ${message.error.code}: ${message.error.message}`);
|
|
218
|
+
}
|
|
219
|
+
result = message.result as T;
|
|
220
|
+
}
|
|
221
|
+
// Handle notifications
|
|
222
|
+
else if ("method" in message && !("id" in message)) {
|
|
223
|
+
const notification = message as { method: string; params?: unknown };
|
|
224
|
+
this.onNotification?.(notification.method, notification.params);
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error && error.message.startsWith("MCP error")) {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
// Ignore other parse errors
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (result === undefined) {
|
|
237
|
+
throw new Error("No response received");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
244
|
+
if (!this._connected) {
|
|
245
|
+
throw new Error("Transport not connected");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body = {
|
|
249
|
+
jsonrpc: "2.0" as const,
|
|
250
|
+
method,
|
|
251
|
+
params: params ?? {},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const headers: Record<string, string> = {
|
|
255
|
+
"Content-Type": "application/json",
|
|
256
|
+
Accept: "application/json, text/event-stream",
|
|
257
|
+
...this.config.headers,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (this.sessionId) {
|
|
261
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await fetch(this.config.url, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 202 Accepted is success for notifications
|
|
271
|
+
if (!response.ok && response.status !== 202) {
|
|
272
|
+
const text = await response.text();
|
|
273
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async close(): Promise<void> {
|
|
278
|
+
if (!this._connected) return;
|
|
279
|
+
this._connected = false;
|
|
280
|
+
|
|
281
|
+
// Abort SSE listener
|
|
282
|
+
if (this.sseConnection) {
|
|
283
|
+
this.sseConnection.abort();
|
|
284
|
+
this.sseConnection = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Send session termination if we have a session
|
|
288
|
+
if (this.sessionId) {
|
|
289
|
+
try {
|
|
290
|
+
const headers: Record<string, string> = {
|
|
291
|
+
...this.config.headers,
|
|
292
|
+
"Mcp-Session-Id": this.sessionId,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
await fetch(this.config.url, {
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
headers,
|
|
298
|
+
});
|
|
299
|
+
} catch {
|
|
300
|
+
// Ignore termination errors
|
|
301
|
+
}
|
|
302
|
+
this.sessionId = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.onClose?.();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create and connect an HTTP transport.
|
|
311
|
+
*/
|
|
312
|
+
export async function createHttpTransport(config: MCPHttpServerConfig | MCPSseServerConfig): Promise<HttpTransport> {
|
|
313
|
+
const transport = new HttpTransport(config);
|
|
314
|
+
await transport.connect();
|
|
315
|
+
return transport;
|
|
316
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* Implements JSON-RPC 2.0 over subprocess stdin/stdout.
|
|
5
|
+
* Messages are newline-delimited JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type Subprocess, spawn } from "bun";
|
|
9
|
+
import type { JsonRpcResponse, MCPStdioServerConfig, MCPTransport } from "../types.js";
|
|
10
|
+
|
|
11
|
+
/** Generate unique request ID */
|
|
12
|
+
function generateId(): string {
|
|
13
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stdio transport for MCP servers.
|
|
18
|
+
* Spawns a subprocess and communicates via stdin/stdout.
|
|
19
|
+
*/
|
|
20
|
+
export class StdioTransport implements MCPTransport {
|
|
21
|
+
private process: Subprocess<"pipe", "pipe", "pipe"> | null = null;
|
|
22
|
+
private pendingRequests = new Map<
|
|
23
|
+
string | number,
|
|
24
|
+
{
|
|
25
|
+
resolve: (value: unknown) => void;
|
|
26
|
+
reject: (error: Error) => void;
|
|
27
|
+
}
|
|
28
|
+
>();
|
|
29
|
+
private buffer = "";
|
|
30
|
+
private _connected = false;
|
|
31
|
+
private readLoop: Promise<void> | null = null;
|
|
32
|
+
|
|
33
|
+
onClose?: () => void;
|
|
34
|
+
onError?: (error: Error) => void;
|
|
35
|
+
onNotification?: (method: string, params: unknown) => void;
|
|
36
|
+
|
|
37
|
+
constructor(private config: MCPStdioServerConfig) {}
|
|
38
|
+
|
|
39
|
+
get connected(): boolean {
|
|
40
|
+
return this._connected;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start the subprocess and begin reading.
|
|
45
|
+
*/
|
|
46
|
+
async connect(): Promise<void> {
|
|
47
|
+
if (this._connected) return;
|
|
48
|
+
|
|
49
|
+
const args = this.config.args ?? [];
|
|
50
|
+
const env = {
|
|
51
|
+
...process.env,
|
|
52
|
+
...this.config.env,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.process = spawn({
|
|
56
|
+
cmd: [this.config.command, ...args],
|
|
57
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
58
|
+
env,
|
|
59
|
+
stdin: "pipe",
|
|
60
|
+
stdout: "pipe",
|
|
61
|
+
stderr: "pipe",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this._connected = true;
|
|
65
|
+
|
|
66
|
+
// Start reading stdout
|
|
67
|
+
this.readLoop = this.startReadLoop();
|
|
68
|
+
|
|
69
|
+
// Log stderr for debugging
|
|
70
|
+
this.startStderrLoop();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async startReadLoop(): Promise<void> {
|
|
74
|
+
if (!this.process?.stdout) return;
|
|
75
|
+
|
|
76
|
+
const reader = this.process.stdout.getReader();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
while (this._connected) {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
|
|
84
|
+
this.buffer += decoder.decode(value, { stream: true });
|
|
85
|
+
this.processBuffer();
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (this._connected) {
|
|
89
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
reader.releaseLock();
|
|
93
|
+
this.handleClose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async startStderrLoop(): Promise<void> {
|
|
98
|
+
if (!this.process?.stderr) return;
|
|
99
|
+
|
|
100
|
+
const reader = this.process.stderr.getReader();
|
|
101
|
+
const decoder = new TextDecoder();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
while (this._connected) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done) break;
|
|
107
|
+
// Log stderr but don't treat as error - servers use it for logging
|
|
108
|
+
const text = decoder.decode(value, { stream: true });
|
|
109
|
+
if (text.trim()) {
|
|
110
|
+
// Could expose via onStderr callback if needed
|
|
111
|
+
// For now, silent - MCP spec says clients MAY capture/ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore stderr read errors
|
|
116
|
+
} finally {
|
|
117
|
+
reader.releaseLock();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private processBuffer(): void {
|
|
122
|
+
const lines = this.buffer.split("\n");
|
|
123
|
+
// Keep incomplete last line in buffer
|
|
124
|
+
this.buffer = lines.pop() ?? "";
|
|
125
|
+
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed) continue;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const message = JSON.parse(trimmed) as JsonRpcResponse;
|
|
132
|
+
this.handleMessage(message);
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore malformed lines
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private handleMessage(message: JsonRpcResponse): void {
|
|
140
|
+
// Check if it's a response (has id)
|
|
141
|
+
if ("id" in message && message.id !== null) {
|
|
142
|
+
const pending = this.pendingRequests.get(message.id);
|
|
143
|
+
if (pending) {
|
|
144
|
+
this.pendingRequests.delete(message.id);
|
|
145
|
+
if (message.error) {
|
|
146
|
+
pending.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
|
|
147
|
+
} else {
|
|
148
|
+
pending.resolve(message.result);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else if ("method" in message) {
|
|
152
|
+
// It's a notification from server
|
|
153
|
+
const notification = message as { method: string; params?: unknown };
|
|
154
|
+
this.onNotification?.(notification.method, notification.params);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleClose(): void {
|
|
159
|
+
if (!this._connected) return;
|
|
160
|
+
this._connected = false;
|
|
161
|
+
|
|
162
|
+
// Reject all pending requests
|
|
163
|
+
for (const [, pending] of this.pendingRequests) {
|
|
164
|
+
pending.reject(new Error("Transport closed"));
|
|
165
|
+
}
|
|
166
|
+
this.pendingRequests.clear();
|
|
167
|
+
|
|
168
|
+
this.onClose?.();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
172
|
+
if (!this._connected || !this.process?.stdin) {
|
|
173
|
+
throw new Error("Transport not connected");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const id = generateId();
|
|
177
|
+
const request = {
|
|
178
|
+
jsonrpc: "2.0" as const,
|
|
179
|
+
id,
|
|
180
|
+
method,
|
|
181
|
+
params: params ?? {},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return new Promise<T>((resolve, reject) => {
|
|
185
|
+
this.pendingRequests.set(id, {
|
|
186
|
+
resolve: resolve as (value: unknown) => void,
|
|
187
|
+
reject,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const message = `${JSON.stringify(request)}\n`;
|
|
191
|
+
try {
|
|
192
|
+
// Bun's FileSink has write() method directly
|
|
193
|
+
this.process!.stdin.write(message);
|
|
194
|
+
this.process!.stdin.flush();
|
|
195
|
+
} catch (error: unknown) {
|
|
196
|
+
this.pendingRequests.delete(id);
|
|
197
|
+
reject(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
203
|
+
if (!this._connected || !this.process?.stdin) {
|
|
204
|
+
throw new Error("Transport not connected");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const notification = {
|
|
208
|
+
jsonrpc: "2.0" as const,
|
|
209
|
+
method,
|
|
210
|
+
params: params ?? {},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const message = `${JSON.stringify(notification)}\n`;
|
|
214
|
+
// Bun's FileSink has write() method directly
|
|
215
|
+
this.process.stdin.write(message);
|
|
216
|
+
this.process.stdin.flush();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async close(): Promise<void> {
|
|
220
|
+
if (!this._connected) return;
|
|
221
|
+
this._connected = false;
|
|
222
|
+
|
|
223
|
+
// Reject pending requests
|
|
224
|
+
for (const [, pending] of this.pendingRequests) {
|
|
225
|
+
pending.reject(new Error("Transport closed"));
|
|
226
|
+
}
|
|
227
|
+
this.pendingRequests.clear();
|
|
228
|
+
|
|
229
|
+
// Kill subprocess
|
|
230
|
+
if (this.process) {
|
|
231
|
+
this.process.kill();
|
|
232
|
+
this.process = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Wait for read loop to finish
|
|
236
|
+
if (this.readLoop) {
|
|
237
|
+
await this.readLoop.catch(() => {});
|
|
238
|
+
this.readLoop = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.onClose?.();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create and connect a stdio transport.
|
|
247
|
+
*/
|
|
248
|
+
export async function createStdioTransport(config: MCPStdioServerConfig): Promise<StdioTransport> {
|
|
249
|
+
const transport = new StdioTransport(config);
|
|
250
|
+
await transport.connect();
|
|
251
|
+
return transport;
|
|
252
|
+
}
|