anyclaude-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/agent.d.ts +110 -0
- package/dist/agent.js +897 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.js +9 -0
- package/dist/background/manager.d.ts +32 -0
- package/dist/background/manager.js +108 -0
- package/dist/background/tools.d.ts +5 -0
- package/dist/background/tools.js +98 -0
- package/dist/background/worker.d.ts +19 -0
- package/dist/background/worker.js +30 -0
- package/dist/commands/builtins.d.ts +2 -0
- package/dist/commands/builtins.js +306 -0
- package/dist/commands/index.d.ts +21 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/types.d.ts +110 -0
- package/dist/commands/types.js +5 -0
- package/dist/compact.d.ts +22 -0
- package/dist/compact.js +67 -0
- package/dist/fs/dexie.d.ts +57 -0
- package/dist/fs/dexie.js +243 -0
- package/dist/fs/index.d.ts +4 -0
- package/dist/fs/index.js +13 -0
- package/dist/fs/linuxTree.d.ts +11 -0
- package/dist/fs/linuxTree.js +43 -0
- package/dist/fs/opfs.d.ts +23 -0
- package/dist/fs/opfs.js +112 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/llm/anthropic.d.ts +24 -0
- package/dist/llm/anthropic.js +280 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/inlineTools.d.ts +11 -0
- package/dist/llm/inlineTools.js +72 -0
- package/dist/llm/openai.d.ts +29 -0
- package/dist/llm/openai.js +224 -0
- package/dist/llm/responses.d.ts +18 -0
- package/dist/llm/responses.js +256 -0
- package/dist/mcp/client.d.ts +20 -0
- package/dist/mcp/client.js +156 -0
- package/dist/mcp/index.d.ts +24 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/proxy.d.ts +3 -0
- package/dist/mcp/proxy.js +25 -0
- package/dist/mcp/sdkServer.d.ts +21 -0
- package/dist/mcp/sdkServer.js +28 -0
- package/dist/mcp/types.d.ts +92 -0
- package/dist/mcp/types.js +5 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/render.d.ts +7 -0
- package/dist/memory/render.js +46 -0
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +79 -0
- package/dist/memory/tools.d.ts +5 -0
- package/dist/memory/tools.js +95 -0
- package/dist/memory/types.d.ts +15 -0
- package/dist/memory/types.js +4 -0
- package/dist/permissions/dangerous.d.ts +4 -0
- package/dist/permissions/dangerous.js +24 -0
- package/dist/permissions/gate.d.ts +21 -0
- package/dist/permissions/gate.js +66 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +6 -0
- package/dist/permissions/match.d.ts +19 -0
- package/dist/permissions/match.js +104 -0
- package/dist/permissions/planMode.d.ts +3 -0
- package/dist/permissions/planMode.js +33 -0
- package/dist/permissions/types.d.ts +19 -0
- package/dist/permissions/types.js +2 -0
- package/dist/persist.d.ts +15 -0
- package/dist/persist.js +58 -0
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +34 -0
- package/dist/query.d.ts +105 -0
- package/dist/query.js +115 -0
- package/dist/queue.d.ts +23 -0
- package/dist/queue.js +43 -0
- package/dist/sandbox/cloudflare.d.ts +48 -0
- package/dist/sandbox/cloudflare.js +124 -0
- package/dist/sandbox/daytona.d.ts +48 -0
- package/dist/sandbox/daytona.js +79 -0
- package/dist/sandbox/e2b.d.ts +54 -0
- package/dist/sandbox/e2b.js +87 -0
- package/dist/sandbox/index.d.ts +8 -0
- package/dist/sandbox/index.js +19 -0
- package/dist/sandbox/local.d.ts +51 -0
- package/dist/sandbox/local.js +155 -0
- package/dist/sandbox/types.d.ts +18 -0
- package/dist/sandbox/types.js +27 -0
- package/dist/sandbox/util.d.ts +15 -0
- package/dist/sandbox/util.js +100 -0
- package/dist/sandbox/vercel.d.ts +48 -0
- package/dist/sandbox/vercel.js +130 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +6 -0
- package/dist/session/store.d.ts +28 -0
- package/dist/session/store.js +122 -0
- package/dist/session/types.d.ts +22 -0
- package/dist/session/types.js +2 -0
- package/dist/settings/index.d.ts +3 -0
- package/dist/settings/index.js +3 -0
- package/dist/settings/load.d.ts +20 -0
- package/dist/settings/load.js +36 -0
- package/dist/settings/merge.d.ts +13 -0
- package/dist/settings/merge.js +65 -0
- package/dist/settings/types.d.ts +17 -0
- package/dist/settings/types.js +3 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +5 -0
- package/dist/skills/load.d.ts +23 -0
- package/dist/skills/load.js +54 -0
- package/dist/skills/parse.d.ts +7 -0
- package/dist/skills/parse.js +40 -0
- package/dist/skills/tool.d.ts +2 -0
- package/dist/skills/tool.js +39 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/skills/types.js +4 -0
- package/dist/team/dispatch.d.ts +2 -0
- package/dist/team/dispatch.js +41 -0
- package/dist/team/index.d.ts +9 -0
- package/dist/team/index.js +11 -0
- package/dist/team/mailbox.d.ts +24 -0
- package/dist/team/mailbox.js +33 -0
- package/dist/team/prompt.d.ts +1 -0
- package/dist/team/prompt.js +12 -0
- package/dist/team/runner.d.ts +20 -0
- package/dist/team/runner.js +45 -0
- package/dist/team/taskBoard.d.ts +41 -0
- package/dist/team/taskBoard.js +73 -0
- package/dist/team/tools.d.ts +7 -0
- package/dist/team/tools.js +190 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +45 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +44 -0
- package/dist/tools/define.d.ts +18 -0
- package/dist/tools/define.js +21 -0
- package/dist/tools/delete_file.d.ts +2 -0
- package/dist/tools/delete_file.js +33 -0
- package/dist/tools/edit_file.d.ts +2 -0
- package/dist/tools/edit_file.js +93 -0
- package/dist/tools/fileTypes.d.ts +32 -0
- package/dist/tools/fileTypes.js +166 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +53 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +110 -0
- package/dist/tools/imageProcessor.d.ts +15 -0
- package/dist/tools/imageProcessor.js +83 -0
- package/dist/tools/index.d.ts +28 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/list_files.d.ts +2 -0
- package/dist/tools/list_files.js +42 -0
- package/dist/tools/multi_edit.d.ts +2 -0
- package/dist/tools/multi_edit.js +112 -0
- package/dist/tools/notebook_edit.d.ts +2 -0
- package/dist/tools/notebook_edit.js +118 -0
- package/dist/tools/plan_mode.d.ts +4 -0
- package/dist/tools/plan_mode.js +44 -0
- package/dist/tools/read_file.d.ts +2 -0
- package/dist/tools/read_file.js +193 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.js +77 -0
- package/dist/tools/todo_write.d.ts +2 -0
- package/dist/tools/todo_write.js +104 -0
- package/dist/tools/tool_search.d.ts +2 -0
- package/dist/tools/tool_search.js +49 -0
- package/dist/tools/types.d.ts +82 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/walk.d.ts +29 -0
- package/dist/tools/walk.js +82 -0
- package/dist/tools/web_fetch.d.ts +2 -0
- package/dist/tools/web_fetch.js +76 -0
- package/dist/tools/web_search.d.ts +22 -0
- package/dist/tools/web_search.js +195 -0
- package/dist/tools/write_file.d.ts +2 -0
- package/dist/tools/write_file.js +39 -0
- package/dist/types/index.d.ts +363 -0
- package/dist/types/index.js +9 -0
- package/dist/util/ids.d.ts +3 -0
- package/dist/util/ids.js +22 -0
- package/dist/util/paths.d.ts +16 -0
- package/dist/util/paths.js +72 -0
- package/dist/util/pricing.d.ts +15 -0
- package/dist/util/pricing.js +81 -0
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/memory.d.ts +28 -0
- package/dist/workspace/memory.js +97 -0
- package/dist/workspace/webcontainer.d.ts +65 -0
- package/dist/workspace/webcontainer.js +156 -0
- package/package.json +78 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { parseInlineToolCalls } from './inlineTools.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an LLMClient backed by any OpenAI-compatible /chat/completions
|
|
4
|
+
* endpoint (OpenAI, Groq, Together, OpenRouter, local llama.cpp, etc.).
|
|
5
|
+
*
|
|
6
|
+
* Streams text deltas through `onToken` and surfaces tool calls (assembled
|
|
7
|
+
* from streamed deltas) via the returned StreamResult and the `onTool` hook.
|
|
8
|
+
*/
|
|
9
|
+
export function createOpenAIClient(options = {}) {
|
|
10
|
+
const baseUrl = (options.baseUrl ?? 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
11
|
+
const defaultModel = options.model ?? 'gpt-4o';
|
|
12
|
+
return {
|
|
13
|
+
async streamChat(messages, opts) {
|
|
14
|
+
const model = opts.model || defaultModel;
|
|
15
|
+
const body = {
|
|
16
|
+
model,
|
|
17
|
+
messages: messages.map(toOpenAIMessage),
|
|
18
|
+
stream: true,
|
|
19
|
+
stream_options: { include_usage: true },
|
|
20
|
+
};
|
|
21
|
+
if (options.temperature !== undefined)
|
|
22
|
+
body.temperature = options.temperature;
|
|
23
|
+
if (options.maxTokens !== undefined)
|
|
24
|
+
body.max_tokens = options.maxTokens;
|
|
25
|
+
// Reasoning models (e.g. xAI grok-4.x): 'none' → 0 reasoning tokens (cheaper/faster).
|
|
26
|
+
if (options.reasoningEffort)
|
|
27
|
+
body.reasoning_effort = options.reasoningEffort;
|
|
28
|
+
if (opts.tools?.length) {
|
|
29
|
+
body.tools = opts.tools;
|
|
30
|
+
body.tool_choice = 'auto';
|
|
31
|
+
if (options.parallelToolCalls !== undefined)
|
|
32
|
+
body.parallel_tool_calls = options.parallelToolCalls;
|
|
33
|
+
}
|
|
34
|
+
const apiKey = typeof options.apiKey === 'function' ? options.apiKey() : options.apiKey;
|
|
35
|
+
const headers = {
|
|
36
|
+
'content-type': 'application/json',
|
|
37
|
+
...options.headers,
|
|
38
|
+
};
|
|
39
|
+
if (apiKey)
|
|
40
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
41
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
signal: opts.signal,
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok || !res.body) {
|
|
48
|
+
const errText = await res.text().catch(() => res.statusText);
|
|
49
|
+
throw new Error(`LLM request failed (${res.status}): ${errText}`);
|
|
50
|
+
}
|
|
51
|
+
let text = '';
|
|
52
|
+
let usage;
|
|
53
|
+
let finishReason = null;
|
|
54
|
+
const toolAcc = new Map();
|
|
55
|
+
await consumeSSE(res.body, (data) => {
|
|
56
|
+
if (data === '[DONE]')
|
|
57
|
+
return;
|
|
58
|
+
let chunk;
|
|
59
|
+
try {
|
|
60
|
+
chunk = JSON.parse(data);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// The final usage chunk (stream_options.include_usage) has empty choices.
|
|
66
|
+
if (chunk.usage) {
|
|
67
|
+
usage = {
|
|
68
|
+
input_tokens: chunk.usage.prompt_tokens ?? 0,
|
|
69
|
+
output_tokens: chunk.usage.completion_tokens ?? 0,
|
|
70
|
+
cache_read_input_tokens: chunk.usage.prompt_tokens_details?.cached_tokens,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const choice = chunk.choices?.[0];
|
|
74
|
+
if (choice?.finish_reason)
|
|
75
|
+
finishReason = choice.finish_reason;
|
|
76
|
+
const delta = choice?.delta;
|
|
77
|
+
if (!delta)
|
|
78
|
+
return;
|
|
79
|
+
if (typeof delta.content === 'string' && delta.content) {
|
|
80
|
+
text += delta.content;
|
|
81
|
+
opts.onToken(delta.content);
|
|
82
|
+
}
|
|
83
|
+
if (delta.tool_calls) {
|
|
84
|
+
for (const tc of delta.tool_calls) {
|
|
85
|
+
const idx = tc.index ?? 0;
|
|
86
|
+
const cur = toolAcc.get(idx) ?? { id: '', name: '', args: '' };
|
|
87
|
+
if (tc.id)
|
|
88
|
+
cur.id = tc.id;
|
|
89
|
+
if (tc.function?.name)
|
|
90
|
+
cur.name = tc.function.name;
|
|
91
|
+
if (tc.function?.arguments)
|
|
92
|
+
cur.args += tc.function.arguments;
|
|
93
|
+
toolAcc.set(idx, cur);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const toolCalls = [...toolAcc.entries()]
|
|
98
|
+
.sort(([a], [b]) => a - b)
|
|
99
|
+
.map(([idx, t]) => ({
|
|
100
|
+
id: t.id || `call_${idx}`,
|
|
101
|
+
type: 'function',
|
|
102
|
+
function: { name: t.name, arguments: t.args || '{}' },
|
|
103
|
+
}));
|
|
104
|
+
// Fallback: some endpoints emit tool calls as inline text rather than
|
|
105
|
+
// native tool_calls. Parse them out and clean the visible text.
|
|
106
|
+
let finalText = text;
|
|
107
|
+
if (!toolCalls.length) {
|
|
108
|
+
const inline = parseInlineToolCalls(text);
|
|
109
|
+
if (inline.calls.length) {
|
|
110
|
+
toolCalls.push(...inline.calls);
|
|
111
|
+
finalText = inline.cleanedText;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (toolCalls.length && opts.onTool)
|
|
115
|
+
opts.onTool(toolCalls);
|
|
116
|
+
return { text: finalText, toolCalls, model, usage, stopReason: mapFinishReason(finishReason) };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Map OpenAI finish_reason to our StopReason vocabulary. */
|
|
121
|
+
function mapFinishReason(reason) {
|
|
122
|
+
switch (reason) {
|
|
123
|
+
case 'stop':
|
|
124
|
+
return 'end_turn';
|
|
125
|
+
case 'length':
|
|
126
|
+
return 'max_tokens';
|
|
127
|
+
case 'tool_calls':
|
|
128
|
+
case 'function_call':
|
|
129
|
+
return 'tool_use';
|
|
130
|
+
default:
|
|
131
|
+
return reason ? 'end_turn' : null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Convert our provider-neutral ChatMsg into an OpenAI chat message. */
|
|
135
|
+
function toOpenAIMessage(msg) {
|
|
136
|
+
if (msg.role === 'tool') {
|
|
137
|
+
return {
|
|
138
|
+
role: 'tool',
|
|
139
|
+
tool_call_id: msg.tool_call_id,
|
|
140
|
+
content: typeof msg.content === 'string' ? msg.content : blocksToText(msg.content),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (msg.role === 'assistant') {
|
|
144
|
+
const out = {
|
|
145
|
+
role: 'assistant',
|
|
146
|
+
content: typeof msg.content === 'string' ? msg.content : blocksToText(msg.content),
|
|
147
|
+
};
|
|
148
|
+
if (msg.tool_calls?.length)
|
|
149
|
+
out.tool_calls = msg.tool_calls;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
// system / user
|
|
153
|
+
if (typeof msg.content === 'string') {
|
|
154
|
+
return { role: msg.role, content: msg.content };
|
|
155
|
+
}
|
|
156
|
+
return { role: msg.role, content: blocksToOpenAIContent(msg.content) };
|
|
157
|
+
}
|
|
158
|
+
function blocksToOpenAIContent(blocks) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
for (const b of blocks) {
|
|
161
|
+
if (b.type === 'text')
|
|
162
|
+
parts.push({ type: 'text', text: b.text });
|
|
163
|
+
else if (b.type === 'image') {
|
|
164
|
+
parts.push({
|
|
165
|
+
type: 'image_url',
|
|
166
|
+
image_url: { url: `data:${b.source.media_type};base64,${b.source.data}` },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (b.type === 'document') {
|
|
170
|
+
// OpenAI chat completions has no portable inline-PDF part; some gateways
|
|
171
|
+
// accept a `file` part. Emit that when we have base64 PDF data, plus a
|
|
172
|
+
// text marker so non-supporting models still get a hint.
|
|
173
|
+
if (b.source.type === 'base64') {
|
|
174
|
+
parts.push({
|
|
175
|
+
type: 'file',
|
|
176
|
+
file: {
|
|
177
|
+
filename: b.title ?? 'document.pdf',
|
|
178
|
+
file_data: `data:application/pdf;base64,${b.source.data}`,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
parts.push({ type: 'text', text: b.source.data });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return parts.length ? parts : '';
|
|
188
|
+
}
|
|
189
|
+
function blocksToText(blocks) {
|
|
190
|
+
return blocks
|
|
191
|
+
.map((b) => {
|
|
192
|
+
if (b.type === 'text')
|
|
193
|
+
return b.text;
|
|
194
|
+
if (b.type === 'tool_result')
|
|
195
|
+
return typeof b.content === 'string' ? b.content : blocksToText(b.content);
|
|
196
|
+
return '';
|
|
197
|
+
})
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join('\n');
|
|
200
|
+
}
|
|
201
|
+
/** Read an SSE response body, invoking `onData` for each `data:` payload. */
|
|
202
|
+
export async function consumeSSE(body, onData) {
|
|
203
|
+
const reader = body.getReader();
|
|
204
|
+
const decoder = new TextDecoder();
|
|
205
|
+
let buffer = '';
|
|
206
|
+
try {
|
|
207
|
+
for (;;) {
|
|
208
|
+
const { value, done } = await reader.read();
|
|
209
|
+
if (done)
|
|
210
|
+
break;
|
|
211
|
+
buffer += decoder.decode(value, { stream: true });
|
|
212
|
+
let nl;
|
|
213
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
214
|
+
const line = buffer.slice(0, nl).trim();
|
|
215
|
+
buffer = buffer.slice(nl + 1);
|
|
216
|
+
if (line.startsWith('data:'))
|
|
217
|
+
onData(line.slice(5).trim());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
reader.releaseLock?.();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LLMClient } from '../types/index.js';
|
|
2
|
+
export interface ResponsesClientOptions {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
/** Base URL of the Responses-compatible API. Default: https://api.openai.com/v1 */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
/** Max output tokens (maps to `max_output_tokens`). */
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
/** Whether the server should persist state. Default false (we send full history). */
|
|
12
|
+
store?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates an LLMClient backed by the OpenAI Responses API (or any
|
|
16
|
+
* Responses-compatible endpoint).
|
|
17
|
+
*/
|
|
18
|
+
export declare function createResponsesClient(options?: ResponsesClientOptions): LLMClient;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// OpenAI Responses API client (POST /v1/responses).
|
|
2
|
+
//
|
|
3
|
+
// The Responses API differs from Chat Completions:
|
|
4
|
+
// - System text goes in `instructions`; the conversation is an `input` array
|
|
5
|
+
// of typed items (messages, function_call, function_call_output).
|
|
6
|
+
// - Tools are flat: { type:'function', name, description, parameters }.
|
|
7
|
+
// - Streaming is a typed SSE event stream: response.output_text.delta,
|
|
8
|
+
// response.function_call_arguments.delta, response.output_item.added,
|
|
9
|
+
// response.completed, etc.
|
|
10
|
+
//
|
|
11
|
+
// We normalize all of that to our provider-neutral LLMClient interface.
|
|
12
|
+
import { consumeSSE } from './openai.js';
|
|
13
|
+
import { parseInlineToolCalls } from './inlineTools.js';
|
|
14
|
+
/**
|
|
15
|
+
* Creates an LLMClient backed by the OpenAI Responses API (or any
|
|
16
|
+
* Responses-compatible endpoint).
|
|
17
|
+
*/
|
|
18
|
+
export function createResponsesClient(options = {}) {
|
|
19
|
+
const baseUrl = (options.baseUrl ?? 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
20
|
+
const defaultModel = options.model ?? 'gpt-4o';
|
|
21
|
+
return {
|
|
22
|
+
async streamChat(messages, opts) {
|
|
23
|
+
const model = opts.model || defaultModel;
|
|
24
|
+
const { instructions, input } = toResponsesInput(messages);
|
|
25
|
+
const body = {
|
|
26
|
+
model,
|
|
27
|
+
input,
|
|
28
|
+
stream: true,
|
|
29
|
+
store: options.store ?? false,
|
|
30
|
+
};
|
|
31
|
+
if (instructions)
|
|
32
|
+
body.instructions = instructions;
|
|
33
|
+
if (options.temperature !== undefined)
|
|
34
|
+
body.temperature = options.temperature;
|
|
35
|
+
if (options.maxTokens !== undefined)
|
|
36
|
+
body.max_output_tokens = options.maxTokens;
|
|
37
|
+
if (opts.tools?.length) {
|
|
38
|
+
body.tools = opts.tools.map(toResponsesTool);
|
|
39
|
+
body.tool_choice = 'auto';
|
|
40
|
+
}
|
|
41
|
+
const res = await fetch(`${baseUrl}/responses`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
signal: opts.signal,
|
|
44
|
+
headers: {
|
|
45
|
+
'content-type': 'application/json',
|
|
46
|
+
...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
|
|
47
|
+
...options.headers,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok || !res.body) {
|
|
52
|
+
const errText = await res.text().catch(() => res.statusText);
|
|
53
|
+
throw new Error(`Responses request failed (${res.status}): ${errText}`);
|
|
54
|
+
}
|
|
55
|
+
let text = '';
|
|
56
|
+
let usage;
|
|
57
|
+
let status = null;
|
|
58
|
+
// Function calls stream as output items keyed by output_index.
|
|
59
|
+
const toolAcc = new Map();
|
|
60
|
+
await consumeSSE(res.body, (data) => {
|
|
61
|
+
if (data === '[DONE]')
|
|
62
|
+
return;
|
|
63
|
+
let ev;
|
|
64
|
+
try {
|
|
65
|
+
ev = JSON.parse(data);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
switch (ev.type) {
|
|
71
|
+
case 'response.output_text.delta': {
|
|
72
|
+
if (typeof ev.delta === 'string' && ev.delta) {
|
|
73
|
+
text += ev.delta;
|
|
74
|
+
opts.onToken(ev.delta);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'response.output_item.added': {
|
|
79
|
+
const item = ev.item;
|
|
80
|
+
if (item?.type === 'function_call') {
|
|
81
|
+
toolAcc.set(ev.output_index ?? toolAcc.size, {
|
|
82
|
+
callId: item.call_id ?? item.id ?? '',
|
|
83
|
+
name: item.name ?? '',
|
|
84
|
+
args: typeof item.arguments === 'string' ? item.arguments : '',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'response.function_call_arguments.delta': {
|
|
90
|
+
const cur = toolAcc.get(ev.output_index ?? 0);
|
|
91
|
+
if (cur && typeof ev.delta === 'string')
|
|
92
|
+
cur.args += ev.delta;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'response.output_item.done': {
|
|
96
|
+
// Finalize args/name/call_id from the completed item.
|
|
97
|
+
const item = ev.item;
|
|
98
|
+
if (item?.type === 'function_call') {
|
|
99
|
+
const cur = toolAcc.get(ev.output_index ?? 0);
|
|
100
|
+
if (cur) {
|
|
101
|
+
if (item.call_id)
|
|
102
|
+
cur.callId = item.call_id;
|
|
103
|
+
if (item.name)
|
|
104
|
+
cur.name = item.name;
|
|
105
|
+
if (typeof item.arguments === 'string' && item.arguments)
|
|
106
|
+
cur.args = item.arguments;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'response.completed':
|
|
112
|
+
case 'response.incomplete':
|
|
113
|
+
case 'response.failed': {
|
|
114
|
+
const r = ev.response;
|
|
115
|
+
if (r?.usage) {
|
|
116
|
+
usage = {
|
|
117
|
+
input_tokens: r.usage.input_tokens ?? 0,
|
|
118
|
+
output_tokens: r.usage.output_tokens ?? 0,
|
|
119
|
+
cache_read_input_tokens: r.usage.input_tokens_details?.cached_tokens,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
status = r?.status ?? null;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
let toolCalls = [...toolAcc.entries()]
|
|
130
|
+
.sort(([a], [b]) => a - b)
|
|
131
|
+
.map(([idx, t]) => ({
|
|
132
|
+
id: t.callId || `call_${idx}`,
|
|
133
|
+
type: 'function',
|
|
134
|
+
function: { name: t.name, arguments: t.args || '{}' },
|
|
135
|
+
}));
|
|
136
|
+
let finalText = text;
|
|
137
|
+
if (!toolCalls.length) {
|
|
138
|
+
const inline = parseInlineToolCalls(text);
|
|
139
|
+
if (inline.calls.length) {
|
|
140
|
+
toolCalls = inline.calls;
|
|
141
|
+
finalText = inline.cleanedText;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (toolCalls.length && opts.onTool)
|
|
145
|
+
opts.onTool(toolCalls);
|
|
146
|
+
const stopReason = toolCalls.length
|
|
147
|
+
? 'tool_use'
|
|
148
|
+
: status === 'incomplete'
|
|
149
|
+
? 'max_tokens'
|
|
150
|
+
: 'end_turn';
|
|
151
|
+
return { text: finalText, toolCalls, model, usage, stopReason };
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Convert our ChatMsg[] into Responses `instructions` + `input` items. */
|
|
156
|
+
function toResponsesInput(messages) {
|
|
157
|
+
const instructions = [];
|
|
158
|
+
const input = [];
|
|
159
|
+
for (const msg of messages) {
|
|
160
|
+
if (msg.role === 'system') {
|
|
161
|
+
instructions.push(typeof msg.content === 'string' ? msg.content : blocksToText(msg.content));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (msg.role === 'tool') {
|
|
165
|
+
input.push({
|
|
166
|
+
type: 'function_call_output',
|
|
167
|
+
call_id: msg.tool_call_id,
|
|
168
|
+
output: typeof msg.content === 'string' ? msg.content : blocksToText(msg.content),
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (msg.role === 'assistant') {
|
|
173
|
+
const textContent = typeof msg.content === 'string' ? msg.content : blocksToText(msg.content);
|
|
174
|
+
if (textContent) {
|
|
175
|
+
input.push({
|
|
176
|
+
role: 'assistant',
|
|
177
|
+
content: [{ type: 'output_text', text: textContent }],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (msg.tool_calls?.length) {
|
|
181
|
+
for (const tc of msg.tool_calls) {
|
|
182
|
+
input.push({
|
|
183
|
+
type: 'function_call',
|
|
184
|
+
call_id: tc.id,
|
|
185
|
+
name: tc.function.name,
|
|
186
|
+
arguments: tc.function.arguments || '{}',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// user
|
|
193
|
+
if (typeof msg.content === 'string') {
|
|
194
|
+
input.push({ role: 'user', content: [{ type: 'input_text', text: msg.content }] });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
input.push({ role: 'user', content: toInputParts(msg.content) });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { instructions: instructions.join('\n\n'), input };
|
|
201
|
+
}
|
|
202
|
+
/** Map our content blocks to Responses input content parts. */
|
|
203
|
+
function toInputParts(blocks) {
|
|
204
|
+
const parts = [];
|
|
205
|
+
for (const b of blocks) {
|
|
206
|
+
if (b.type === 'text') {
|
|
207
|
+
parts.push({ type: 'input_text', text: b.text });
|
|
208
|
+
}
|
|
209
|
+
else if (b.type === 'image') {
|
|
210
|
+
parts.push({
|
|
211
|
+
type: 'input_image',
|
|
212
|
+
image_url: `data:${b.source.media_type};base64,${b.source.data}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else if (b.type === 'document') {
|
|
216
|
+
if (b.source.type === 'base64') {
|
|
217
|
+
parts.push({
|
|
218
|
+
type: 'input_file',
|
|
219
|
+
filename: b.title ?? 'document.pdf',
|
|
220
|
+
file_data: `data:application/pdf;base64,${b.source.data}`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
parts.push({ type: 'input_text', text: b.source.data });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (b.type === 'tool_result') {
|
|
228
|
+
parts.push({
|
|
229
|
+
type: 'input_text',
|
|
230
|
+
text: typeof b.content === 'string' ? b.content : blocksToText(b.content),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return parts.length ? parts : [{ type: 'input_text', text: '' }];
|
|
235
|
+
}
|
|
236
|
+
/** Convert an OpenAI-shape ToolDef into a Responses (flat) tool definition. */
|
|
237
|
+
function toResponsesTool(tool) {
|
|
238
|
+
return {
|
|
239
|
+
type: 'function',
|
|
240
|
+
name: tool.function.name,
|
|
241
|
+
description: tool.function.description,
|
|
242
|
+
parameters: tool.function.parameters,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function blocksToText(blocks) {
|
|
246
|
+
return blocks
|
|
247
|
+
.map((b) => {
|
|
248
|
+
if (b.type === 'text')
|
|
249
|
+
return b.text;
|
|
250
|
+
if (b.type === 'tool_result')
|
|
251
|
+
return typeof b.content === 'string' ? b.content : blocksToText(b.content);
|
|
252
|
+
return '';
|
|
253
|
+
})
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join('\n');
|
|
256
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { McpHttpServerConfig, McpSSEServerConfig, McpToolInfo, McpToolResult } from './types.js';
|
|
2
|
+
import { type McpProxy } from './proxy.js';
|
|
3
|
+
export declare class McpClient {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The actual URL we fetch (target, optionally rewritten through a proxy). */
|
|
6
|
+
private url;
|
|
7
|
+
private headers;
|
|
8
|
+
private sessionId;
|
|
9
|
+
private nextId;
|
|
10
|
+
private connected;
|
|
11
|
+
constructor(config: McpHttpServerConfig | McpSSEServerConfig, name: string, proxy?: McpProxy);
|
|
12
|
+
/** initialize handshake, then send the initialized notification. */
|
|
13
|
+
connect(signal?: AbortSignal): Promise<void>;
|
|
14
|
+
listTools(signal?: AbortSignal): Promise<McpToolInfo[]>;
|
|
15
|
+
callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<McpToolResult>;
|
|
16
|
+
private buildHeaders;
|
|
17
|
+
private request;
|
|
18
|
+
/** Fire-and-forget notification (no id, no response expected). */
|
|
19
|
+
private notify;
|
|
20
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// MCP client for remote servers over the Streamable HTTP transport.
|
|
2
|
+
//
|
|
3
|
+
// Implements the JSON-RPC handshake (initialize → notifications/initialized) and
|
|
4
|
+
// the tools/list + tools/call methods. Browser-safe: uses only global fetch,
|
|
5
|
+
// TextDecoder, and ReadableStream.
|
|
6
|
+
//
|
|
7
|
+
// Transport note: both 'http' and 'sse' configs are driven by POSTing JSON-RPC
|
|
8
|
+
// to a single endpoint. A response may come back as a plain JSON body or as a
|
|
9
|
+
// `text/event-stream` (SSE) — we handle both and match responses by request id.
|
|
10
|
+
// Most MCP servers (including legacy "sse" servers) accept POSTed requests this
|
|
11
|
+
// way; the dedicated GET event channel is not required for request/response.
|
|
12
|
+
import { applyProxy } from './proxy.js';
|
|
13
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
14
|
+
const FALLBACK_PROTOCOL_VERSION = '2024-11-05';
|
|
15
|
+
const CLIENT_INFO = { name: 'browser-claude-sdk', version: '0.1.0' };
|
|
16
|
+
export class McpClient {
|
|
17
|
+
constructor(config, name, proxy) {
|
|
18
|
+
this.sessionId = null;
|
|
19
|
+
this.nextId = 1;
|
|
20
|
+
this.connected = false;
|
|
21
|
+
this.name = name;
|
|
22
|
+
this.url = applyProxy(config.url, proxy);
|
|
23
|
+
this.headers = { ...(config.headers ?? {}) };
|
|
24
|
+
}
|
|
25
|
+
/** initialize handshake, then send the initialized notification. */
|
|
26
|
+
async connect(signal) {
|
|
27
|
+
if (this.connected)
|
|
28
|
+
return;
|
|
29
|
+
let initResult;
|
|
30
|
+
try {
|
|
31
|
+
initResult = await this.request('initialize', {
|
|
32
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
33
|
+
capabilities: {},
|
|
34
|
+
clientInfo: CLIENT_INFO,
|
|
35
|
+
}, signal);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
// Retry once with the older protocol version for legacy servers.
|
|
39
|
+
initResult = await this.request('initialize', {
|
|
40
|
+
protocolVersion: FALLBACK_PROTOCOL_VERSION,
|
|
41
|
+
capabilities: {},
|
|
42
|
+
clientInfo: CLIENT_INFO,
|
|
43
|
+
}, signal);
|
|
44
|
+
}
|
|
45
|
+
if (initResult.error) {
|
|
46
|
+
throw new Error(`MCP initialize failed: ${initResult.error.message}`);
|
|
47
|
+
}
|
|
48
|
+
await this.notify('notifications/initialized');
|
|
49
|
+
this.connected = true;
|
|
50
|
+
}
|
|
51
|
+
async listTools(signal) {
|
|
52
|
+
const tools = [];
|
|
53
|
+
let cursor;
|
|
54
|
+
do {
|
|
55
|
+
const res = await this.request('tools/list', cursor ? { cursor } : {}, signal);
|
|
56
|
+
if (res.error)
|
|
57
|
+
throw new Error(`tools/list failed: ${res.error.message}`);
|
|
58
|
+
const result = (res.result ?? {});
|
|
59
|
+
if (Array.isArray(result.tools))
|
|
60
|
+
tools.push(...result.tools);
|
|
61
|
+
cursor = result.nextCursor;
|
|
62
|
+
} while (cursor);
|
|
63
|
+
return tools;
|
|
64
|
+
}
|
|
65
|
+
async callTool(name, args, signal) {
|
|
66
|
+
const res = await this.request('tools/call', { name, arguments: args ?? {} }, signal);
|
|
67
|
+
if (res.error)
|
|
68
|
+
throw new Error(`tools/call (${name}) failed: ${res.error.message}`);
|
|
69
|
+
const result = (res.result ?? { content: [] });
|
|
70
|
+
if (!Array.isArray(result.content))
|
|
71
|
+
result.content = [];
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// ---- transport ----
|
|
75
|
+
buildHeaders() {
|
|
76
|
+
const h = {
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
accept: 'application/json, text/event-stream',
|
|
79
|
+
...this.headers,
|
|
80
|
+
};
|
|
81
|
+
if (this.sessionId)
|
|
82
|
+
h['mcp-session-id'] = this.sessionId;
|
|
83
|
+
return h;
|
|
84
|
+
}
|
|
85
|
+
async request(method, params, signal) {
|
|
86
|
+
const id = this.nextId++;
|
|
87
|
+
const res = await fetch(this.url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
signal,
|
|
90
|
+
headers: this.buildHeaders(),
|
|
91
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
92
|
+
});
|
|
93
|
+
const sid = res.headers.get('mcp-session-id');
|
|
94
|
+
if (sid)
|
|
95
|
+
this.sessionId = sid;
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const txt = await res.text().catch(() => res.statusText);
|
|
98
|
+
throw new Error(`MCP HTTP ${res.status} for ${method}: ${txt}`);
|
|
99
|
+
}
|
|
100
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
101
|
+
if (contentType.includes('text/event-stream') && res.body) {
|
|
102
|
+
return await readSSEForId(res.body, id);
|
|
103
|
+
}
|
|
104
|
+
// Plain JSON response.
|
|
105
|
+
return (await res.json());
|
|
106
|
+
}
|
|
107
|
+
/** Fire-and-forget notification (no id, no response expected). */
|
|
108
|
+
async notify(method, params) {
|
|
109
|
+
try {
|
|
110
|
+
await fetch(this.url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: this.buildHeaders(),
|
|
113
|
+
body: JSON.stringify({ jsonrpc: '2.0', method, params }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Notifications are best-effort.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Read an SSE body and resolve the first JSON-RPC message matching `id`. */
|
|
122
|
+
async function readSSEForId(body, id) {
|
|
123
|
+
const reader = body.getReader();
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
let buffer = '';
|
|
126
|
+
try {
|
|
127
|
+
for (;;) {
|
|
128
|
+
const { value, done } = await reader.read();
|
|
129
|
+
if (done)
|
|
130
|
+
break;
|
|
131
|
+
buffer += decoder.decode(value, { stream: true });
|
|
132
|
+
let nl;
|
|
133
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
134
|
+
const line = buffer.slice(0, nl).trim();
|
|
135
|
+
buffer = buffer.slice(nl + 1);
|
|
136
|
+
if (!line.startsWith('data:'))
|
|
137
|
+
continue;
|
|
138
|
+
const data = line.slice(5).trim();
|
|
139
|
+
if (!data || data === '[DONE]')
|
|
140
|
+
continue;
|
|
141
|
+
try {
|
|
142
|
+
const msg = JSON.parse(data);
|
|
143
|
+
if (msg && msg.id === id)
|
|
144
|
+
return msg;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// partial/non-JSON keepalive; keep reading
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
reader.releaseLock?.();
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`MCP SSE stream ended without a response for request ${id}`);
|
|
156
|
+
}
|