@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.
Files changed (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. 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,6 @@
1
+ /**
2
+ * MCP transport exports.
3
+ */
4
+
5
+ export { createHttpTransport, HttpTransport } from "./http.js";
6
+ export { createStdioTransport, StdioTransport } from "./stdio.js";
@@ -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
+ }