@ruvnet/open-claude-code 2.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/package.json +27 -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/config/cli-args.mjs +139 -0
- package/src/config/env.mjs +261 -0
- package/src/config/settings.mjs +119 -0
- package/src/core/agent-loop.mjs +462 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/checkpoints.mjs +141 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session.mjs +143 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +220 -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/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/tools/agent.mjs +127 -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 +127 -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 +114 -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/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/commands.mjs +499 -0
- package/src/ui/ink-app.mjs +231 -0
- package/src/ui/repl.mjs +144 -0
- package/test/test.mjs +1857 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* open-claude-code v2
|
|
4
|
+
*
|
|
5
|
+
* Open source implementation of Claude Code CLI architecture.
|
|
6
|
+
* Based on ruDevolution decompilation of Claude Code v2.1.91.
|
|
7
|
+
*
|
|
8
|
+
* Architecture mirrors the actual Claude Code internals:
|
|
9
|
+
* - Async generator agent loop (13 event types)
|
|
10
|
+
* - 25+ tools with validateInput/call interface
|
|
11
|
+
* - MCP client (stdio/SSE/WS/sHTTP transports)
|
|
12
|
+
* - 6 permission modes + sandbox
|
|
13
|
+
* - Context compaction + auto-compaction
|
|
14
|
+
* - Hooks system (7 events)
|
|
15
|
+
* - Settings chain (5 layers, 76 properties)
|
|
16
|
+
* - Multi-provider support (Anthropic, OpenAI, Google)
|
|
17
|
+
* - Custom agents and skills
|
|
18
|
+
* - Session management and checkpoints
|
|
19
|
+
* - Prompt caching
|
|
20
|
+
* - 39 slash commands
|
|
21
|
+
* - Telemetry stub
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createAgentLoop } from './core/agent-loop.mjs';
|
|
25
|
+
import { createToolRegistry } from './tools/registry.mjs';
|
|
26
|
+
import { createPermissionChecker } from './permissions/checker.mjs';
|
|
27
|
+
import { loadSettings } from './config/settings.mjs';
|
|
28
|
+
import { parseArgs, getUsageText } from './config/cli-args.mjs';
|
|
29
|
+
import { HookEngine } from './hooks/engine.mjs';
|
|
30
|
+
import { McpClient } from './mcp/client.mjs';
|
|
31
|
+
import { AgentLoader } from './agents/loader.mjs';
|
|
32
|
+
import { SkillsLoader } from './skills/loader.mjs';
|
|
33
|
+
import { SessionManager } from './core/session.mjs';
|
|
34
|
+
import { CheckpointManager } from './core/checkpoints.mjs';
|
|
35
|
+
import { PromptCache } from './core/cache.mjs';
|
|
36
|
+
import { readEnv } from './config/env.mjs';
|
|
37
|
+
import * as telemetry from './telemetry/index.mjs';
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const args = parseArgs(process.argv.slice(2));
|
|
41
|
+
|
|
42
|
+
// Handle --version
|
|
43
|
+
if (args.showVersion) {
|
|
44
|
+
console.log('open-claude-code v2.0.0-alpha.1');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle --help
|
|
49
|
+
if (args.showHelp) {
|
|
50
|
+
console.log(getUsageText());
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const settings = await loadSettings();
|
|
55
|
+
const env = readEnv();
|
|
56
|
+
|
|
57
|
+
// Apply CLI overrides to settings
|
|
58
|
+
if (args.permissionMode) settings.permissions = { ...settings.permissions, defaultMode: args.permissionMode };
|
|
59
|
+
if (args.systemPrompt) settings.systemPromptOverride = args.systemPrompt;
|
|
60
|
+
if (args.addDirs?.length) settings.addDirs = args.addDirs;
|
|
61
|
+
if (args.maxTurns) settings.maxTurns = args.maxTurns;
|
|
62
|
+
if (args.verbose) settings.verbose = true;
|
|
63
|
+
if (args.debug) settings.debug = true;
|
|
64
|
+
|
|
65
|
+
const tools = createToolRegistry();
|
|
66
|
+
const permissions = createPermissionChecker(settings.permissions);
|
|
67
|
+
const hooks = new HookEngine(settings.hooks);
|
|
68
|
+
|
|
69
|
+
// Apply tool allow/deny lists
|
|
70
|
+
if (args.allowedTools) settings.allowedTools = args.allowedTools;
|
|
71
|
+
if (args.disallowedTools) settings.disallowedTools = args.disallowedTools;
|
|
72
|
+
|
|
73
|
+
// Load custom agents
|
|
74
|
+
const agentLoader = new AgentLoader();
|
|
75
|
+
agentLoader.load();
|
|
76
|
+
|
|
77
|
+
// Load skills
|
|
78
|
+
const skillsLoader = new SkillsLoader();
|
|
79
|
+
skillsLoader.load();
|
|
80
|
+
|
|
81
|
+
// Wire skill tool
|
|
82
|
+
const skillTool = tools.get('Skill');
|
|
83
|
+
if (skillTool) skillTool._skillsLoader = skillsLoader;
|
|
84
|
+
|
|
85
|
+
// Session management
|
|
86
|
+
const sessionManager = new SessionManager();
|
|
87
|
+
const checkpointManager = new CheckpointManager();
|
|
88
|
+
const promptCache = new PromptCache();
|
|
89
|
+
|
|
90
|
+
// Connect MCP servers if configured
|
|
91
|
+
const mcpClients = [];
|
|
92
|
+
if (settings.mcpServers) {
|
|
93
|
+
for (const [name, config] of Object.entries(settings.mcpServers)) {
|
|
94
|
+
try {
|
|
95
|
+
const client = new McpClient(config);
|
|
96
|
+
await client.connect();
|
|
97
|
+
const mcpTools = await client.listTools();
|
|
98
|
+
tools.registerMcpTools(mcpTools, (toolName, toolArgs) => client.callTool(toolName, toolArgs));
|
|
99
|
+
mcpClients.push(client);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(`MCP server "${name}" failed to connect: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Wire MCP resource tool
|
|
107
|
+
const mcpResourceTool = tools.get('ReadMcpResource');
|
|
108
|
+
if (mcpResourceTool) mcpResourceTool._mcpClients = mcpClients;
|
|
109
|
+
|
|
110
|
+
const loop = createAgentLoop({
|
|
111
|
+
model: args.model || settings.model || 'claude-sonnet-4-6',
|
|
112
|
+
tools,
|
|
113
|
+
permissions,
|
|
114
|
+
settings,
|
|
115
|
+
hooks,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Attach extra state for commands to access
|
|
119
|
+
loop.state._agentLoader = agentLoader;
|
|
120
|
+
loop.state._skillsLoader = skillsLoader;
|
|
121
|
+
loop.state._mcpClients = mcpClients;
|
|
122
|
+
loop.state._hooks = settings.hooks;
|
|
123
|
+
loop.state._permissionMode = settings.permissions?.defaultMode || 'default';
|
|
124
|
+
loop.state._sessionManager = sessionManager;
|
|
125
|
+
loop.state._checkpointManager = checkpointManager;
|
|
126
|
+
loop.state._promptCache = promptCache;
|
|
127
|
+
|
|
128
|
+
telemetry.track('session.start', { model: loop.state.model });
|
|
129
|
+
|
|
130
|
+
// Graceful shutdown
|
|
131
|
+
const cleanup = async () => {
|
|
132
|
+
telemetry.track('session.end', {
|
|
133
|
+
turns: loop.state.turnCount,
|
|
134
|
+
tokens: loop.state.tokenUsage,
|
|
135
|
+
});
|
|
136
|
+
for (const client of mcpClients) {
|
|
137
|
+
await client.disconnect().catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
process.on('SIGINT', async () => { await cleanup(); process.exit(0); });
|
|
141
|
+
process.on('SIGTERM', async () => { await cleanup(); process.exit(0); });
|
|
142
|
+
|
|
143
|
+
if (args.prompt) {
|
|
144
|
+
// Non-interactive: run prompt and exit
|
|
145
|
+
const outputFormat = args.outputFormat || 'text';
|
|
146
|
+
const results = [];
|
|
147
|
+
|
|
148
|
+
for await (const event of loop.run(args.prompt)) {
|
|
149
|
+
if (outputFormat === 'json') {
|
|
150
|
+
results.push(event);
|
|
151
|
+
} else if (outputFormat === 'stream-json') {
|
|
152
|
+
console.log(JSON.stringify(event));
|
|
153
|
+
} else {
|
|
154
|
+
handleEvent(event, settings);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (outputFormat === 'json') {
|
|
159
|
+
// Extract final text
|
|
160
|
+
const texts = results
|
|
161
|
+
.filter(e => e.type === 'assistant')
|
|
162
|
+
.map(e => e.content)
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
console.log(JSON.stringify({
|
|
165
|
+
result: texts.join('\n'),
|
|
166
|
+
usage: loop.state.tokenUsage,
|
|
167
|
+
model: loop.state.model,
|
|
168
|
+
}));
|
|
169
|
+
} else {
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await cleanup();
|
|
174
|
+
} else {
|
|
175
|
+
// Interactive REPL
|
|
176
|
+
const { startRepl } = await import('./ui/repl.mjs');
|
|
177
|
+
await startRepl(loop, settings);
|
|
178
|
+
await cleanup();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleEvent(event, settings = {}) {
|
|
183
|
+
switch (event.type) {
|
|
184
|
+
case 'stream_request_start':
|
|
185
|
+
break;
|
|
186
|
+
case 'stream_event':
|
|
187
|
+
process.stdout.write(event.text || '');
|
|
188
|
+
break;
|
|
189
|
+
case 'thinking':
|
|
190
|
+
if (process.env.SHOW_THINKING || settings.verbose) {
|
|
191
|
+
process.stdout.write(`\x1b[2m${event.text}\x1b[0m`);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'assistant':
|
|
195
|
+
if (!event._streamed && event.content) console.log(event.content);
|
|
196
|
+
break;
|
|
197
|
+
case 'tool_progress':
|
|
198
|
+
process.stderr.write(`\x1b[33m[${event.tool}]\x1b[0m running...\n`);
|
|
199
|
+
break;
|
|
200
|
+
case 'result':
|
|
201
|
+
break;
|
|
202
|
+
case 'compaction':
|
|
203
|
+
process.stderr.write(`\x1b[2m[compaction #${event.count}]\x1b[0m\n`);
|
|
204
|
+
break;
|
|
205
|
+
case 'hookPermissionResult':
|
|
206
|
+
if (!event.allowed) {
|
|
207
|
+
process.stderr.write(`\x1b[31m[blocked: ${event.tool}]\x1b[0m\n`);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case 'error':
|
|
211
|
+
console.error(`\x1b[31mError: ${event.message}\x1b[0m`);
|
|
212
|
+
break;
|
|
213
|
+
case 'stop':
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch(e => { console.error(e); process.exit(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
|
+
}
|