@oh-my-pi/pi-coding-agent 0.1.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 +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -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 +670 -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/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 +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -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 +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -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 +199 -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/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -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 +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -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 +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -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 +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -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-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -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";
|
|
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";
|
|
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
|
+
}
|