@axplusb/kepler 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client — multi-transport Model Context Protocol client.
|
|
3
|
+
*
|
|
4
|
+
* Supports four transports:
|
|
5
|
+
* - stdio: spawn child process, communicate via stdin/stdout
|
|
6
|
+
* - sse: Server-Sent Events over HTTP
|
|
7
|
+
* - websocket: bidirectional WebSocket
|
|
8
|
+
* - streamable-http: POST with SSE response (new MCP transport)
|
|
9
|
+
*
|
|
10
|
+
* Auto-detects transport from server config.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
|
|
15
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
16
|
+
|
|
17
|
+
export class McpClient {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} serverConfig - { command, args, env, url, transport }
|
|
20
|
+
*/
|
|
21
|
+
constructor(serverConfig) {
|
|
22
|
+
this.config = serverConfig;
|
|
23
|
+
this.process = null;
|
|
24
|
+
this.transport = null;
|
|
25
|
+
this.requestId = 0;
|
|
26
|
+
this.pending = new Map();
|
|
27
|
+
this.buffer = '';
|
|
28
|
+
this.tools = [];
|
|
29
|
+
this.resources = [];
|
|
30
|
+
this.serverInfo = null;
|
|
31
|
+
this.connected = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_detectTransport() {
|
|
35
|
+
if (this.config.transport) return this.config.transport;
|
|
36
|
+
if (this.config.command) return 'stdio';
|
|
37
|
+
if (this.config.url) {
|
|
38
|
+
if (this.config.url.startsWith('ws://') || this.config.url.startsWith('wss://')) return 'websocket';
|
|
39
|
+
if (this.config.url.includes('/sse')) return 'sse';
|
|
40
|
+
return 'streamable-http';
|
|
41
|
+
}
|
|
42
|
+
return 'stdio';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async connect() {
|
|
46
|
+
const transportType = this._detectTransport();
|
|
47
|
+
|
|
48
|
+
switch (transportType) {
|
|
49
|
+
case 'stdio': return this._connectStdio();
|
|
50
|
+
case 'sse': return this._connectSSE();
|
|
51
|
+
case 'websocket': return this._connectWebSocket();
|
|
52
|
+
case 'streamable-http': return this._connectStreamableHttp();
|
|
53
|
+
default: throw new Error(`Unknown MCP transport: ${transportType}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async _connectStdio() {
|
|
58
|
+
this.process = spawn(this.config.command, this.config.args || [], {
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
env: { ...process.env, ...this.config.env },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.process.stdout.on('data', (data) => this._onData(data));
|
|
64
|
+
this.process.stderr.on('data', (data) => {
|
|
65
|
+
if (process.env.MCP_DEBUG) {
|
|
66
|
+
process.stderr.write(`[mcp:${this.config.command}] ${data}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.process.on('exit', (code) => {
|
|
71
|
+
this.connected = false;
|
|
72
|
+
for (const [, { reject }] of this.pending) {
|
|
73
|
+
reject(new Error(`MCP server exited with code ${code}`));
|
|
74
|
+
}
|
|
75
|
+
this.pending.clear();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const initResult = await this._request('initialize', {
|
|
79
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
80
|
+
capabilities: {},
|
|
81
|
+
clientInfo: { name: 'open-claude-code', version: '2.0.0' },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.serverInfo = initResult;
|
|
85
|
+
this.connected = true;
|
|
86
|
+
this._notify('notifications/initialized', {});
|
|
87
|
+
return this.serverInfo;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async _connectSSE() {
|
|
91
|
+
const { SseTransport } = await import('./transport-sse.mjs');
|
|
92
|
+
this.transport = new SseTransport(this.config.url, {
|
|
93
|
+
headers: this.config.headers || {},
|
|
94
|
+
});
|
|
95
|
+
await this.transport.connect();
|
|
96
|
+
this.transport.onMessage((msg) => {
|
|
97
|
+
if (msg.data?.id && this.pending.has(msg.data.id)) {
|
|
98
|
+
const { resolve, reject } = this.pending.get(msg.data.id);
|
|
99
|
+
this.pending.delete(msg.data.id);
|
|
100
|
+
if (msg.data.error) reject(new Error(msg.data.error.message));
|
|
101
|
+
else resolve(msg.data.result);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.connected = true;
|
|
105
|
+
return this._initRemote();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async _connectWebSocket() {
|
|
109
|
+
const { WebSocketTransport } = await import('./transport-ws.mjs');
|
|
110
|
+
this.transport = new WebSocketTransport(this.config.url, {
|
|
111
|
+
headers: this.config.headers || {},
|
|
112
|
+
});
|
|
113
|
+
await this.transport.connect();
|
|
114
|
+
this.connected = true;
|
|
115
|
+
this.serverInfo = await this.transport.request('initialize', {
|
|
116
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
117
|
+
capabilities: {},
|
|
118
|
+
clientInfo: { name: 'open-claude-code', version: '2.0.0' },
|
|
119
|
+
});
|
|
120
|
+
return this.serverInfo;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async _connectStreamableHttp() {
|
|
124
|
+
const { StreamableHttpTransport } = await import('./transport-shttp.mjs');
|
|
125
|
+
this.transport = new StreamableHttpTransport(this.config.url, {
|
|
126
|
+
headers: this.config.headers || {},
|
|
127
|
+
});
|
|
128
|
+
await this.transport.connect();
|
|
129
|
+
this.connected = true;
|
|
130
|
+
this.serverInfo = await this.transport.request('initialize', {
|
|
131
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
132
|
+
capabilities: {},
|
|
133
|
+
clientInfo: { name: 'open-claude-code', version: '2.0.0' },
|
|
134
|
+
});
|
|
135
|
+
return this.serverInfo;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _initRemote() {
|
|
139
|
+
const result = await this._transportRequest('initialize', {
|
|
140
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
141
|
+
capabilities: {},
|
|
142
|
+
clientInfo: { name: 'open-claude-code', version: '2.0.0' },
|
|
143
|
+
});
|
|
144
|
+
this.serverInfo = result;
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _transportRequest(method, params) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const id = ++this.requestId;
|
|
151
|
+
this.pending.set(id, { resolve, reject });
|
|
152
|
+
this.transport.send({ jsonrpc: '2.0', id, method, params });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async listTools() {
|
|
157
|
+
let result;
|
|
158
|
+
if (this.transport?.request) {
|
|
159
|
+
result = await this.transport.request('tools/list', {});
|
|
160
|
+
} else if (this.transport) {
|
|
161
|
+
result = await this._transportRequest('tools/list', {});
|
|
162
|
+
} else {
|
|
163
|
+
result = await this._request('tools/list', {});
|
|
164
|
+
}
|
|
165
|
+
this.tools = result?.tools || [];
|
|
166
|
+
return this.tools;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async callTool(name, args) {
|
|
170
|
+
const params = { name, arguments: args };
|
|
171
|
+
let result;
|
|
172
|
+
if (this.transport?.request) {
|
|
173
|
+
result = await this.transport.request('tools/call', params);
|
|
174
|
+
} else if (this.transport) {
|
|
175
|
+
result = await this._transportRequest('tools/call', params);
|
|
176
|
+
} else {
|
|
177
|
+
result = await this._request('tools/call', params);
|
|
178
|
+
}
|
|
179
|
+
if (result?.content && Array.isArray(result.content)) {
|
|
180
|
+
return result.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async readResource(uri) {
|
|
186
|
+
const params = { uri };
|
|
187
|
+
if (this.transport?.request) return this.transport.request('resources/read', params);
|
|
188
|
+
if (this.transport) return this._transportRequest('resources/read', params);
|
|
189
|
+
return this._request('resources/read', params);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async listResources() {
|
|
193
|
+
let result;
|
|
194
|
+
if (this.transport?.request) result = await this.transport.request('resources/list', {});
|
|
195
|
+
else if (this.transport) result = await this._transportRequest('resources/list', {});
|
|
196
|
+
else result = await this._request('resources/list', {});
|
|
197
|
+
this.resources = result?.resources || [];
|
|
198
|
+
return this.resources;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async disconnect() {
|
|
202
|
+
this.connected = false;
|
|
203
|
+
if (this.transport) {
|
|
204
|
+
await this.transport.disconnect();
|
|
205
|
+
this.transport = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!this.process) return;
|
|
209
|
+
try {
|
|
210
|
+
await this._request('shutdown', {});
|
|
211
|
+
this._notify('exit', {});
|
|
212
|
+
} catch { /* best effort */ }
|
|
213
|
+
|
|
214
|
+
await new Promise(resolve => {
|
|
215
|
+
const timeout = setTimeout(() => { this.process?.kill('SIGKILL'); resolve(); }, 2000);
|
|
216
|
+
this.process.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
217
|
+
this.process.kill('SIGTERM');
|
|
218
|
+
});
|
|
219
|
+
this.process = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_request(method, params) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const id = ++this.requestId;
|
|
225
|
+
this.pending.set(id, { resolve, reject });
|
|
226
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
227
|
+
this.process.stdin.write(msg + '\n');
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_notify(method, params) {
|
|
232
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', method, params });
|
|
233
|
+
this.process?.stdin.write(msg + '\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_onData(data) {
|
|
237
|
+
this.buffer += data.toString();
|
|
238
|
+
const lines = this.buffer.split('\n');
|
|
239
|
+
this.buffer = lines.pop();
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
if (!line.trim()) continue;
|
|
242
|
+
try {
|
|
243
|
+
const msg = JSON.parse(line);
|
|
244
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
245
|
+
const { resolve, reject } = this.pending.get(msg.id);
|
|
246
|
+
this.pending.delete(msg.id);
|
|
247
|
+
if (msg.error) reject(new Error(`MCP error: ${msg.error.message || JSON.stringify(msg.error)}`));
|
|
248
|
+
else resolve(msg.result);
|
|
249
|
+
}
|
|
250
|
+
} catch { /* malformed */ }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP Transport — new MCP transport.
|
|
3
|
+
*
|
|
4
|
+
* Uses POST with SSE response body for bidirectional communication.
|
|
5
|
+
* Each request is a POST that returns a streaming SSE response.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class StreamableHttpTransport {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} url - HTTP endpoint URL
|
|
11
|
+
* @param {object} [options] - { headers, timeout, sessionId }
|
|
12
|
+
*/
|
|
13
|
+
constructor(url, options = {}) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.headers = options.headers || {};
|
|
16
|
+
this.timeout = options.timeout || 30000;
|
|
17
|
+
this.sessionId = options.sessionId || null;
|
|
18
|
+
this.connected = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async connect() {
|
|
22
|
+
// Test connectivity with an empty request
|
|
23
|
+
const res = await fetch(this.url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
Accept: 'text/event-stream',
|
|
28
|
+
...this.headers,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', params: {} }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
this.sessionId = res.headers.get('x-session-id') || this.sessionId;
|
|
35
|
+
this.connected = true;
|
|
36
|
+
}
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Send a JSON-RPC request and receive a streaming SSE response.
|
|
42
|
+
* Collects all events and returns the final result.
|
|
43
|
+
*/
|
|
44
|
+
async request(method, params) {
|
|
45
|
+
const id = Date.now();
|
|
46
|
+
const body = { jsonrpc: '2.0', id, method, params };
|
|
47
|
+
|
|
48
|
+
const headers = {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Accept: 'text/event-stream',
|
|
51
|
+
...this.headers,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (this.sessionId) {
|
|
55
|
+
headers['x-session-id'] = this.sessionId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const res = await fetch(this.url, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers,
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`sHTTP error: ${res.status} ${res.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const contentType = res.headers.get('content-type') || '';
|
|
69
|
+
|
|
70
|
+
// If SSE response, stream it
|
|
71
|
+
if (contentType.includes('text/event-stream')) {
|
|
72
|
+
return this._readSSEResponse(res);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Otherwise, plain JSON
|
|
76
|
+
return res.json();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async _readSSEResponse(res) {
|
|
80
|
+
const reader = res.body.getReader();
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let buffer = '';
|
|
83
|
+
let result = null;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if (done) break;
|
|
89
|
+
|
|
90
|
+
buffer += decoder.decode(value, { stream: true });
|
|
91
|
+
const events = buffer.split('\n\n');
|
|
92
|
+
buffer = events.pop() || '';
|
|
93
|
+
|
|
94
|
+
for (const raw of events) {
|
|
95
|
+
const parsed = this._parseEvent(raw);
|
|
96
|
+
if (parsed?.data?.result !== undefined) {
|
|
97
|
+
result = parsed.data.result;
|
|
98
|
+
} else if (parsed?.data?.error) {
|
|
99
|
+
throw new Error(parsed.data.error.message || 'Unknown error');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
reader.releaseLock();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_parseEvent(raw) {
|
|
111
|
+
const dataLines = [];
|
|
112
|
+
for (const line of raw.split('\n')) {
|
|
113
|
+
if (line.startsWith('data: ')) dataLines.push(line.slice(6));
|
|
114
|
+
}
|
|
115
|
+
if (dataLines.length === 0) return null;
|
|
116
|
+
try {
|
|
117
|
+
return { data: JSON.parse(dataLines.join('\n')) };
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async send(message) {
|
|
124
|
+
return this.request(message.method, message.params);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async disconnect() {
|
|
128
|
+
this.connected = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE Transport — Server-Sent Events transport for MCP.
|
|
3
|
+
*
|
|
4
|
+
* Connects to an MCP server over HTTP using SSE for server-to-client
|
|
5
|
+
* messages and POST for client-to-server messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SseTransport {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} url - SSE endpoint URL
|
|
11
|
+
* @param {object} [options] - { headers, timeout }
|
|
12
|
+
*/
|
|
13
|
+
constructor(url, options = {}) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.headers = options.headers || {};
|
|
16
|
+
this.timeout = options.timeout || 30000;
|
|
17
|
+
this.connected = false;
|
|
18
|
+
this.messageHandlers = [];
|
|
19
|
+
this.reader = null;
|
|
20
|
+
this.abortController = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async connect() {
|
|
24
|
+
this.abortController = new AbortController();
|
|
25
|
+
|
|
26
|
+
const res = await fetch(this.url, {
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: 'text/event-stream',
|
|
29
|
+
...this.headers,
|
|
30
|
+
},
|
|
31
|
+
signal: this.abortController.signal,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`SSE connect failed: HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.connected = true;
|
|
39
|
+
this.reader = res.body.getReader();
|
|
40
|
+
this._readLoop();
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _readLoop() {
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
let buffer = '';
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
while (this.connected) {
|
|
50
|
+
const { done, value } = await this.reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
|
|
53
|
+
buffer += decoder.decode(value, { stream: true });
|
|
54
|
+
const events = buffer.split('\n\n');
|
|
55
|
+
buffer = events.pop() || '';
|
|
56
|
+
|
|
57
|
+
for (const raw of events) {
|
|
58
|
+
const parsed = this._parseSSE(raw);
|
|
59
|
+
if (parsed) {
|
|
60
|
+
for (const handler of this.messageHandlers) {
|
|
61
|
+
handler(parsed);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.name !== 'AbortError') {
|
|
68
|
+
this._handleError(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_parseSSE(raw) {
|
|
74
|
+
let eventType = 'message';
|
|
75
|
+
const dataLines = [];
|
|
76
|
+
|
|
77
|
+
for (const line of raw.split('\n')) {
|
|
78
|
+
if (line.startsWith('event: ')) eventType = line.slice(7).trim();
|
|
79
|
+
else if (line.startsWith('data: ')) dataLines.push(line.slice(6));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dataLines.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return {
|
|
86
|
+
type: eventType,
|
|
87
|
+
data: JSON.parse(dataLines.join('\n')),
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return { type: eventType, data: dataLines.join('\n') };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async send(message) {
|
|
95
|
+
const res = await fetch(this.url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
...this.headers,
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(message),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`SSE send failed: HTTP ${res.status}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
} catch {
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onMessage(handler) {
|
|
117
|
+
this.messageHandlers.push(handler);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_handleError(err) {
|
|
121
|
+
if (process.env.MCP_DEBUG) {
|
|
122
|
+
console.error(`[SSE transport] Error: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async disconnect() {
|
|
127
|
+
this.connected = false;
|
|
128
|
+
this.abortController?.abort();
|
|
129
|
+
this.reader = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Transport — bidirectional MCP transport over WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* Uses the ws protocol for full-duplex communication with MCP servers.
|
|
5
|
+
* Falls back to a stub if WebSocket is not available in the runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class WebSocketTransport {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} url - WebSocket URL (ws:// or wss://)
|
|
11
|
+
* @param {object} [options] - { headers, timeout, protocols }
|
|
12
|
+
*/
|
|
13
|
+
constructor(url, options = {}) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.options = options;
|
|
16
|
+
this.timeout = options.timeout || 30000;
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.connected = false;
|
|
19
|
+
this.requestId = 0;
|
|
20
|
+
this.pending = new Map();
|
|
21
|
+
this.messageHandlers = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async connect() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
try {
|
|
27
|
+
// Use global WebSocket if available (Node 22+)
|
|
28
|
+
const WS = globalThis.WebSocket;
|
|
29
|
+
if (!WS) {
|
|
30
|
+
throw new Error('WebSocket not available in this runtime');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.ws = new WS(this.url, this.options.protocols);
|
|
34
|
+
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
reject(new Error('WebSocket connection timeout'));
|
|
37
|
+
}, this.timeout);
|
|
38
|
+
|
|
39
|
+
this.ws.onopen = () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
this.connected = true;
|
|
42
|
+
resolve(this);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.ws.onmessage = (event) => {
|
|
46
|
+
this._handleMessage(event.data);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.ws.onerror = (err) => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
reject(new Error(`WebSocket error: ${err.message || 'unknown'}`));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.ws.onclose = () => {
|
|
55
|
+
this.connected = false;
|
|
56
|
+
for (const [, { reject: rej }] of this.pending) {
|
|
57
|
+
rej(new Error('WebSocket closed'));
|
|
58
|
+
}
|
|
59
|
+
this.pending.clear();
|
|
60
|
+
};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
reject(err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_handleMessage(data) {
|
|
68
|
+
try {
|
|
69
|
+
const msg = JSON.parse(typeof data === 'string' ? data : data.toString());
|
|
70
|
+
|
|
71
|
+
// Handle response to pending request
|
|
72
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
73
|
+
const { resolve, reject } = this.pending.get(msg.id);
|
|
74
|
+
this.pending.delete(msg.id);
|
|
75
|
+
if (msg.error) {
|
|
76
|
+
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
77
|
+
} else {
|
|
78
|
+
resolve(msg.result);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle notifications
|
|
84
|
+
for (const handler of this.messageHandlers) {
|
|
85
|
+
handler(msg);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Malformed message
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async request(method, params) {
|
|
93
|
+
if (!this.connected) throw new Error('WebSocket not connected');
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const id = ++this.requestId;
|
|
97
|
+
this.pending.set(id, { resolve, reject });
|
|
98
|
+
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
this.pending.delete(id);
|
|
101
|
+
reject(new Error(`WebSocket request timeout: ${method}`));
|
|
102
|
+
}, this.timeout);
|
|
103
|
+
|
|
104
|
+
const origResolve = resolve;
|
|
105
|
+
this.pending.set(id, {
|
|
106
|
+
resolve: (val) => { clearTimeout(timeout); origResolve(val); },
|
|
107
|
+
reject: (err) => { clearTimeout(timeout); reject(err); },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.ws.send(JSON.stringify({
|
|
111
|
+
jsonrpc: '2.0',
|
|
112
|
+
id,
|
|
113
|
+
method,
|
|
114
|
+
params: params || {},
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async send(message) {
|
|
120
|
+
return this.request(message.method, message.params);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onMessage(handler) {
|
|
124
|
+
this.messageHandlers.push(handler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async disconnect() {
|
|
128
|
+
this.connected = false;
|
|
129
|
+
if (this.ws) {
|
|
130
|
+
this.ws.close();
|
|
131
|
+
this.ws = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Checker — 6 modes from decompiled Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Integrates with prompt system for interactive permission in default mode,
|
|
5
|
+
* injection checking for Bash commands, and path validation for file ops.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { requiresPermission } from './prompt.mjs';
|
|
9
|
+
import { checkInjection } from './injection-check.mjs';
|
|
10
|
+
import { validatePath } from './path-check.mjs';
|
|
11
|
+
|
|
12
|
+
export function createPermissionChecker(config = {}) {
|
|
13
|
+
const mode = config.defaultMode || process.env.CLAUDE_CODE_PERMISSION_MODE || 'default';
|
|
14
|
+
const rl = config.rl || null; // readline interface for prompts
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
mode,
|
|
18
|
+
async check(toolName, input) {
|
|
19
|
+
// Always run injection check on Bash commands
|
|
20
|
+
if (toolName === 'Bash' && input?.command) {
|
|
21
|
+
const injection = checkInjection(input.command);
|
|
22
|
+
if (!injection.safe) {
|
|
23
|
+
return false; // block dangerous commands
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Always validate file paths for file operations
|
|
28
|
+
if (['Edit', 'Write', 'Read', 'MultiEdit'].includes(toolName) && input?.file_path) {
|
|
29
|
+
const pathResult = validatePath(input.file_path, { write: toolName !== 'Read' });
|
|
30
|
+
if (!pathResult.safe) {
|
|
31
|
+
return false; // block unsafe paths
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (mode) {
|
|
36
|
+
case 'bypassPermissions': return true;
|
|
37
|
+
case 'acceptEdits':
|
|
38
|
+
// Allow file ops, block Bash/Agent unless rl available
|
|
39
|
+
if (toolName === 'Bash' || toolName === 'Agent') {
|
|
40
|
+
return !requiresPermission(toolName) || !!config.bypassBash;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
case 'auto': return true; // AI decides
|
|
44
|
+
case 'dontAsk': return false; // deny everything not pre-approved
|
|
45
|
+
case 'plan': return toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep';
|
|
46
|
+
case 'default':
|
|
47
|
+
default:
|
|
48
|
+
// In default mode, safe tools pass through
|
|
49
|
+
if (!requiresPermission(toolName)) return true;
|
|
50
|
+
// Without a readline interface, allow (headless mode)
|
|
51
|
+
if (!rl) return true;
|
|
52
|
+
// With rl, would call promptPermission — but that's async/interactive
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|