@matthesketh/utopia-ai 0.1.0 → 0.2.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/dist/adapters/anthropic.cjs +20 -13
- package/dist/adapters/anthropic.js +20 -13
- package/dist/adapters/google.cjs +17 -11
- package/dist/adapters/google.js +17 -11
- package/dist/adapters/ollama.cjs +21 -17
- package/dist/adapters/ollama.js +21 -17
- package/dist/adapters/openai.cjs +18 -7
- package/dist/adapters/openai.js +18 -7
- package/dist/index.cjs +32 -20
- package/dist/index.js +32 -20
- package/dist/mcp/index.cjs +40 -33
- package/dist/mcp/index.d.cts +0 -21
- package/dist/mcp/index.d.ts +0 -21
- package/dist/mcp/index.js +40 -33
- package/package.json +4 -4
|
@@ -37,16 +37,16 @@ function anthropicAdapter(config) {
|
|
|
37
37
|
let client = null;
|
|
38
38
|
async function getClient() {
|
|
39
39
|
if (client) return client;
|
|
40
|
-
let
|
|
40
|
+
let AnthropicCtor;
|
|
41
41
|
try {
|
|
42
42
|
const mod = await import("@anthropic-ai/sdk");
|
|
43
|
-
|
|
43
|
+
AnthropicCtor = mod.Anthropic ?? mod.default;
|
|
44
44
|
} catch {
|
|
45
45
|
throw new Error(
|
|
46
46
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
client = new
|
|
49
|
+
client = new AnthropicCtor({
|
|
50
50
|
apiKey: config.apiKey,
|
|
51
51
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
52
52
|
});
|
|
@@ -73,17 +73,22 @@ function anthropicAdapter(config) {
|
|
|
73
73
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
const response = await anthropic.messages.create(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
const response = await anthropic.messages.create(
|
|
77
|
+
body
|
|
78
|
+
);
|
|
79
|
+
if (!response.content || !Array.isArray(response.content)) {
|
|
80
|
+
throw new Error("Anthropic returned invalid response: missing content array");
|
|
81
|
+
}
|
|
82
|
+
const contentBlocks = response.content;
|
|
83
|
+
const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
84
|
+
const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
|
|
85
|
+
const tu = b;
|
|
86
|
+
return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
|
|
87
|
+
});
|
|
83
88
|
return {
|
|
84
89
|
content: textContent,
|
|
85
90
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
86
|
-
finishReason: mapStopReason(response.stop_reason),
|
|
91
|
+
finishReason: mapStopReason(response.stop_reason ?? ""),
|
|
87
92
|
usage: response.usage ? {
|
|
88
93
|
promptTokens: response.usage.input_tokens,
|
|
89
94
|
completionTokens: response.usage.output_tokens,
|
|
@@ -113,7 +118,9 @@ function anthropicAdapter(config) {
|
|
|
113
118
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
const stream = anthropic.messages.stream(
|
|
121
|
+
const stream = anthropic.messages.stream(
|
|
122
|
+
body
|
|
123
|
+
);
|
|
117
124
|
let promptTokens = 0;
|
|
118
125
|
for await (const event of stream) {
|
|
119
126
|
if (event.type === "message_start" && event.message?.usage) {
|
|
@@ -144,7 +151,7 @@ function anthropicAdapter(config) {
|
|
|
144
151
|
const outputTokens = event.usage?.output_tokens ?? 0;
|
|
145
152
|
yield {
|
|
146
153
|
delta: "",
|
|
147
|
-
finishReason: mapStopReason(event.delta.stop_reason),
|
|
154
|
+
finishReason: mapStopReason(event.delta.stop_reason ?? ""),
|
|
148
155
|
usage: event.usage ? {
|
|
149
156
|
promptTokens,
|
|
150
157
|
completionTokens: outputTokens,
|
|
@@ -3,16 +3,16 @@ function anthropicAdapter(config) {
|
|
|
3
3
|
let client = null;
|
|
4
4
|
async function getClient() {
|
|
5
5
|
if (client) return client;
|
|
6
|
-
let
|
|
6
|
+
let AnthropicCtor;
|
|
7
7
|
try {
|
|
8
8
|
const mod = await import("@anthropic-ai/sdk");
|
|
9
|
-
|
|
9
|
+
AnthropicCtor = mod.Anthropic ?? mod.default;
|
|
10
10
|
} catch {
|
|
11
11
|
throw new Error(
|
|
12
12
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
-
client = new
|
|
15
|
+
client = new AnthropicCtor({
|
|
16
16
|
apiKey: config.apiKey,
|
|
17
17
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
18
18
|
});
|
|
@@ -39,17 +39,22 @@ function anthropicAdapter(config) {
|
|
|
39
39
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
const response = await anthropic.messages.create(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const response = await anthropic.messages.create(
|
|
43
|
+
body
|
|
44
|
+
);
|
|
45
|
+
if (!response.content || !Array.isArray(response.content)) {
|
|
46
|
+
throw new Error("Anthropic returned invalid response: missing content array");
|
|
47
|
+
}
|
|
48
|
+
const contentBlocks = response.content;
|
|
49
|
+
const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
50
|
+
const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
|
|
51
|
+
const tu = b;
|
|
52
|
+
return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
|
|
53
|
+
});
|
|
49
54
|
return {
|
|
50
55
|
content: textContent,
|
|
51
56
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
52
|
-
finishReason: mapStopReason(response.stop_reason),
|
|
57
|
+
finishReason: mapStopReason(response.stop_reason ?? ""),
|
|
53
58
|
usage: response.usage ? {
|
|
54
59
|
promptTokens: response.usage.input_tokens,
|
|
55
60
|
completionTokens: response.usage.output_tokens,
|
|
@@ -79,7 +84,9 @@ function anthropicAdapter(config) {
|
|
|
79
84
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
const stream = anthropic.messages.stream(
|
|
87
|
+
const stream = anthropic.messages.stream(
|
|
88
|
+
body
|
|
89
|
+
);
|
|
83
90
|
let promptTokens = 0;
|
|
84
91
|
for await (const event of stream) {
|
|
85
92
|
if (event.type === "message_start" && event.message?.usage) {
|
|
@@ -110,7 +117,7 @@ function anthropicAdapter(config) {
|
|
|
110
117
|
const outputTokens = event.usage?.output_tokens ?? 0;
|
|
111
118
|
yield {
|
|
112
119
|
delta: "",
|
|
113
|
-
finishReason: mapStopReason(event.delta.stop_reason),
|
|
120
|
+
finishReason: mapStopReason(event.delta.stop_reason ?? ""),
|
|
114
121
|
usage: event.usage ? {
|
|
115
122
|
promptTokens,
|
|
116
123
|
completionTokens: outputTokens,
|
package/dist/adapters/google.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(google_exports, {
|
|
|
33
33
|
googleAdapter: () => googleAdapter
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(google_exports);
|
|
36
|
+
var toolCallCounter = 0;
|
|
36
37
|
function googleAdapter(config) {
|
|
37
38
|
let genAI = null;
|
|
38
39
|
async function getGenAI() {
|
|
@@ -72,13 +73,16 @@ function googleAdapter(config) {
|
|
|
72
73
|
const response = result.response;
|
|
73
74
|
const candidate = response.candidates?.[0];
|
|
74
75
|
const parts = candidate?.content?.parts ?? [];
|
|
75
|
-
const textParts = parts.filter((p) => p.text).map((p) => p.text);
|
|
76
|
-
const fnCalls = parts.filter((p) => p.functionCall);
|
|
77
|
-
const toolCalls = fnCalls.map((p) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
|
|
77
|
+
const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
|
|
78
|
+
const toolCalls = fnCalls.map((p) => {
|
|
79
|
+
const fc = p.functionCall;
|
|
80
|
+
return {
|
|
81
|
+
id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
|
|
82
|
+
name: fc.name,
|
|
83
|
+
arguments: fc.args ?? {}
|
|
84
|
+
};
|
|
85
|
+
});
|
|
82
86
|
return {
|
|
83
87
|
content: textParts.join(""),
|
|
84
88
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -112,7 +116,7 @@ function googleAdapter(config) {
|
|
|
112
116
|
});
|
|
113
117
|
for await (const chunk of result.stream) {
|
|
114
118
|
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
115
|
-
const text = parts.filter((p) => p.text).map((p) => p.text).join("");
|
|
119
|
+
const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
|
|
116
120
|
const finishReason = chunk.candidates?.[0]?.finishReason;
|
|
117
121
|
yield {
|
|
118
122
|
delta: text,
|
|
@@ -132,9 +136,11 @@ function googleAdapter(config) {
|
|
|
132
136
|
});
|
|
133
137
|
const inputs = Array.isArray(request.input) ? request.input : [request.input];
|
|
134
138
|
const result = await model.batchEmbedContents({
|
|
135
|
-
requests: inputs.map(
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
requests: inputs.map(
|
|
140
|
+
(text) => ({
|
|
141
|
+
content: { parts: [{ text }], role: "user" }
|
|
142
|
+
})
|
|
143
|
+
)
|
|
138
144
|
});
|
|
139
145
|
return {
|
|
140
146
|
embeddings: result.embeddings.map((e) => e.values),
|
package/dist/adapters/google.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapters/google.ts
|
|
2
|
+
var toolCallCounter = 0;
|
|
2
3
|
function googleAdapter(config) {
|
|
3
4
|
let genAI = null;
|
|
4
5
|
async function getGenAI() {
|
|
@@ -38,13 +39,16 @@ function googleAdapter(config) {
|
|
|
38
39
|
const response = result.response;
|
|
39
40
|
const candidate = response.candidates?.[0];
|
|
40
41
|
const parts = candidate?.content?.parts ?? [];
|
|
41
|
-
const textParts = parts.filter((p) => p.text).map((p) => p.text);
|
|
42
|
-
const fnCalls = parts.filter((p) => p.functionCall);
|
|
43
|
-
const toolCalls = fnCalls.map((p) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
|
|
43
|
+
const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
|
|
44
|
+
const toolCalls = fnCalls.map((p) => {
|
|
45
|
+
const fc = p.functionCall;
|
|
46
|
+
return {
|
|
47
|
+
id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
|
|
48
|
+
name: fc.name,
|
|
49
|
+
arguments: fc.args ?? {}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
48
52
|
return {
|
|
49
53
|
content: textParts.join(""),
|
|
50
54
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -78,7 +82,7 @@ function googleAdapter(config) {
|
|
|
78
82
|
});
|
|
79
83
|
for await (const chunk of result.stream) {
|
|
80
84
|
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
81
|
-
const text = parts.filter((p) => p.text).map((p) => p.text).join("");
|
|
85
|
+
const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
|
|
82
86
|
const finishReason = chunk.candidates?.[0]?.finishReason;
|
|
83
87
|
yield {
|
|
84
88
|
delta: text,
|
|
@@ -98,9 +102,11 @@ function googleAdapter(config) {
|
|
|
98
102
|
});
|
|
99
103
|
const inputs = Array.isArray(request.input) ? request.input : [request.input];
|
|
100
104
|
const result = await model.batchEmbedContents({
|
|
101
|
-
requests: inputs.map(
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
requests: inputs.map(
|
|
106
|
+
(text) => ({
|
|
107
|
+
content: { parts: [{ text }], role: "user" }
|
|
108
|
+
})
|
|
109
|
+
)
|
|
104
110
|
});
|
|
105
111
|
return {
|
|
106
112
|
embeddings: result.embeddings.map((e) => e.values),
|
package/dist/adapters/ollama.cjs
CHANGED
|
@@ -23,21 +23,23 @@ __export(ollama_exports, {
|
|
|
23
23
|
ollamaAdapter: () => ollamaAdapter
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(ollama_exports);
|
|
26
|
+
var ollamaToolCallCounter = 0;
|
|
26
27
|
function ollamaAdapter(config = {}) {
|
|
27
28
|
const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
|
|
28
29
|
return {
|
|
29
30
|
async chat(request) {
|
|
30
31
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
32
|
+
const options = {};
|
|
33
|
+
if (request.temperature !== void 0) options.temperature = request.temperature;
|
|
34
|
+
if (request.topP !== void 0) options.top_p = request.topP;
|
|
35
|
+
if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
|
|
36
|
+
if (request.stop) options.stop = request.stop;
|
|
31
37
|
const body = {
|
|
32
38
|
model,
|
|
33
39
|
messages: toOllamaMessages(request.messages),
|
|
34
40
|
stream: false,
|
|
35
|
-
options
|
|
41
|
+
options
|
|
36
42
|
};
|
|
37
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
38
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
39
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
40
|
-
if (request.stop) body.options.stop = request.stop;
|
|
41
43
|
if (request.tools?.length) {
|
|
42
44
|
body.tools = request.tools.map(toOllamaTool);
|
|
43
45
|
}
|
|
@@ -52,13 +54,11 @@ function ollamaAdapter(config = {}) {
|
|
|
52
54
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
53
55
|
}
|
|
54
56
|
const data = await response.json();
|
|
55
|
-
const toolCalls = (data.message?.tool_calls ?? []).map(
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
})
|
|
61
|
-
);
|
|
57
|
+
const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
|
|
58
|
+
id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
|
|
59
|
+
name: tc.function.name,
|
|
60
|
+
arguments: tc.function.arguments ?? {}
|
|
61
|
+
}));
|
|
62
62
|
return {
|
|
63
63
|
content: data.message?.content ?? "",
|
|
64
64
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -73,16 +73,17 @@ function ollamaAdapter(config = {}) {
|
|
|
73
73
|
},
|
|
74
74
|
async *stream(request) {
|
|
75
75
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
76
|
+
const streamOptions = {};
|
|
77
|
+
if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
|
|
78
|
+
if (request.topP !== void 0) streamOptions.top_p = request.topP;
|
|
79
|
+
if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
|
|
80
|
+
if (request.stop) streamOptions.stop = request.stop;
|
|
76
81
|
const body = {
|
|
77
82
|
model,
|
|
78
83
|
messages: toOllamaMessages(request.messages),
|
|
79
84
|
stream: true,
|
|
80
|
-
options:
|
|
85
|
+
options: streamOptions
|
|
81
86
|
};
|
|
82
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
83
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
84
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
85
|
-
if (request.stop) body.options.stop = request.stop;
|
|
86
87
|
if (request.tools?.length) {
|
|
87
88
|
body.tools = request.tools.map(toOllamaTool);
|
|
88
89
|
}
|
|
@@ -96,6 +97,9 @@ function ollamaAdapter(config = {}) {
|
|
|
96
97
|
const text = await response.text();
|
|
97
98
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
98
99
|
}
|
|
100
|
+
if (!response.body) {
|
|
101
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
102
|
+
}
|
|
99
103
|
const reader = response.body.getReader();
|
|
100
104
|
const decoder = new TextDecoder();
|
|
101
105
|
let buffer = "";
|
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// src/adapters/ollama.ts
|
|
2
|
+
var ollamaToolCallCounter = 0;
|
|
2
3
|
function ollamaAdapter(config = {}) {
|
|
3
4
|
const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
|
|
4
5
|
return {
|
|
5
6
|
async chat(request) {
|
|
6
7
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
8
|
+
const options = {};
|
|
9
|
+
if (request.temperature !== void 0) options.temperature = request.temperature;
|
|
10
|
+
if (request.topP !== void 0) options.top_p = request.topP;
|
|
11
|
+
if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
|
|
12
|
+
if (request.stop) options.stop = request.stop;
|
|
7
13
|
const body = {
|
|
8
14
|
model,
|
|
9
15
|
messages: toOllamaMessages(request.messages),
|
|
10
16
|
stream: false,
|
|
11
|
-
options
|
|
17
|
+
options
|
|
12
18
|
};
|
|
13
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
14
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
15
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
16
|
-
if (request.stop) body.options.stop = request.stop;
|
|
17
19
|
if (request.tools?.length) {
|
|
18
20
|
body.tools = request.tools.map(toOllamaTool);
|
|
19
21
|
}
|
|
@@ -28,13 +30,11 @@ function ollamaAdapter(config = {}) {
|
|
|
28
30
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
29
31
|
}
|
|
30
32
|
const data = await response.json();
|
|
31
|
-
const toolCalls = (data.message?.tool_calls ?? []).map(
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
);
|
|
33
|
+
const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
|
|
34
|
+
id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
|
|
35
|
+
name: tc.function.name,
|
|
36
|
+
arguments: tc.function.arguments ?? {}
|
|
37
|
+
}));
|
|
38
38
|
return {
|
|
39
39
|
content: data.message?.content ?? "",
|
|
40
40
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -49,16 +49,17 @@ function ollamaAdapter(config = {}) {
|
|
|
49
49
|
},
|
|
50
50
|
async *stream(request) {
|
|
51
51
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
52
|
+
const streamOptions = {};
|
|
53
|
+
if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
|
|
54
|
+
if (request.topP !== void 0) streamOptions.top_p = request.topP;
|
|
55
|
+
if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
|
|
56
|
+
if (request.stop) streamOptions.stop = request.stop;
|
|
52
57
|
const body = {
|
|
53
58
|
model,
|
|
54
59
|
messages: toOllamaMessages(request.messages),
|
|
55
60
|
stream: true,
|
|
56
|
-
options:
|
|
61
|
+
options: streamOptions
|
|
57
62
|
};
|
|
58
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
59
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
60
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
61
|
-
if (request.stop) body.options.stop = request.stop;
|
|
62
63
|
if (request.tools?.length) {
|
|
63
64
|
body.tools = request.tools.map(toOllamaTool);
|
|
64
65
|
}
|
|
@@ -72,6 +73,9 @@ function ollamaAdapter(config = {}) {
|
|
|
72
73
|
const text = await response.text();
|
|
73
74
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
74
75
|
}
|
|
76
|
+
if (!response.body) {
|
|
77
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
78
|
+
}
|
|
75
79
|
const reader = response.body.getReader();
|
|
76
80
|
const decoder = new TextDecoder();
|
|
77
81
|
let buffer = "";
|
package/dist/adapters/openai.cjs
CHANGED
|
@@ -37,16 +37,16 @@ function openaiAdapter(config) {
|
|
|
37
37
|
let client = null;
|
|
38
38
|
async function getClient() {
|
|
39
39
|
if (client) return client;
|
|
40
|
-
let
|
|
40
|
+
let OpenAICtor;
|
|
41
41
|
try {
|
|
42
42
|
const mod = await import("openai");
|
|
43
|
-
|
|
43
|
+
OpenAICtor = mod.OpenAI ?? mod.default;
|
|
44
44
|
} catch {
|
|
45
45
|
throw new Error(
|
|
46
46
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
client = new
|
|
49
|
+
client = new OpenAICtor({
|
|
50
50
|
apiKey: config.apiKey,
|
|
51
51
|
baseURL: config.baseURL,
|
|
52
52
|
organization: config.organization
|
|
@@ -72,11 +72,20 @@ function openaiAdapter(config) {
|
|
|
72
72
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
const response = await openai.chat.completions.create(
|
|
75
|
+
const response = await openai.chat.completions.create(
|
|
76
|
+
body
|
|
77
|
+
);
|
|
78
|
+
if (!response.choices?.length) {
|
|
79
|
+
throw new Error("OpenAI returned empty choices array");
|
|
80
|
+
}
|
|
76
81
|
const choice = response.choices[0];
|
|
77
82
|
return {
|
|
78
83
|
content: choice.message.content ?? "",
|
|
79
|
-
toolCalls: choice.message.tool_calls?.map(
|
|
84
|
+
toolCalls: choice.message.tool_calls?.map(
|
|
85
|
+
(tc) => fromOpenAIToolCall(
|
|
86
|
+
tc
|
|
87
|
+
)
|
|
88
|
+
),
|
|
80
89
|
finishReason: mapFinishReason(choice.finish_reason),
|
|
81
90
|
usage: response.usage ? {
|
|
82
91
|
promptTokens: response.usage.prompt_tokens,
|
|
@@ -106,7 +115,9 @@ function openaiAdapter(config) {
|
|
|
106
115
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
const stream = await openai.chat.completions.create(
|
|
118
|
+
const stream = await openai.chat.completions.create(
|
|
119
|
+
body
|
|
120
|
+
);
|
|
110
121
|
for await (const chunk of stream) {
|
|
111
122
|
const delta = chunk.choices?.[0]?.delta;
|
|
112
123
|
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
@@ -218,7 +229,7 @@ function toOpenAIToolChoice(choice) {
|
|
|
218
229
|
if (choice && typeof choice === "object" && "name" in choice) {
|
|
219
230
|
return { type: "function", function: { name: choice.name } };
|
|
220
231
|
}
|
|
221
|
-
return
|
|
232
|
+
return void 0;
|
|
222
233
|
}
|
|
223
234
|
function fromOpenAIToolCall(tc) {
|
|
224
235
|
return {
|
package/dist/adapters/openai.js
CHANGED
|
@@ -3,16 +3,16 @@ function openaiAdapter(config) {
|
|
|
3
3
|
let client = null;
|
|
4
4
|
async function getClient() {
|
|
5
5
|
if (client) return client;
|
|
6
|
-
let
|
|
6
|
+
let OpenAICtor;
|
|
7
7
|
try {
|
|
8
8
|
const mod = await import("openai");
|
|
9
|
-
|
|
9
|
+
OpenAICtor = mod.OpenAI ?? mod.default;
|
|
10
10
|
} catch {
|
|
11
11
|
throw new Error(
|
|
12
12
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
-
client = new
|
|
15
|
+
client = new OpenAICtor({
|
|
16
16
|
apiKey: config.apiKey,
|
|
17
17
|
baseURL: config.baseURL,
|
|
18
18
|
organization: config.organization
|
|
@@ -38,11 +38,20 @@ function openaiAdapter(config) {
|
|
|
38
38
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
const response = await openai.chat.completions.create(
|
|
41
|
+
const response = await openai.chat.completions.create(
|
|
42
|
+
body
|
|
43
|
+
);
|
|
44
|
+
if (!response.choices?.length) {
|
|
45
|
+
throw new Error("OpenAI returned empty choices array");
|
|
46
|
+
}
|
|
42
47
|
const choice = response.choices[0];
|
|
43
48
|
return {
|
|
44
49
|
content: choice.message.content ?? "",
|
|
45
|
-
toolCalls: choice.message.tool_calls?.map(
|
|
50
|
+
toolCalls: choice.message.tool_calls?.map(
|
|
51
|
+
(tc) => fromOpenAIToolCall(
|
|
52
|
+
tc
|
|
53
|
+
)
|
|
54
|
+
),
|
|
46
55
|
finishReason: mapFinishReason(choice.finish_reason),
|
|
47
56
|
usage: response.usage ? {
|
|
48
57
|
promptTokens: response.usage.prompt_tokens,
|
|
@@ -72,7 +81,9 @@ function openaiAdapter(config) {
|
|
|
72
81
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
const stream = await openai.chat.completions.create(
|
|
84
|
+
const stream = await openai.chat.completions.create(
|
|
85
|
+
body
|
|
86
|
+
);
|
|
76
87
|
for await (const chunk of stream) {
|
|
77
88
|
const delta = chunk.choices?.[0]?.delta;
|
|
78
89
|
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
@@ -184,7 +195,7 @@ function toOpenAIToolChoice(choice) {
|
|
|
184
195
|
if (choice && typeof choice === "object" && "name" in choice) {
|
|
185
196
|
return { type: "function", function: { name: choice.name } };
|
|
186
197
|
}
|
|
187
|
-
return
|
|
198
|
+
return void 0;
|
|
188
199
|
}
|
|
189
200
|
function fromOpenAIToolCall(tc) {
|
|
190
201
|
return {
|
package/dist/index.cjs
CHANGED
|
@@ -44,7 +44,10 @@ function createAI(adapter, options) {
|
|
|
44
44
|
}
|
|
45
45
|
return response;
|
|
46
46
|
} catch (err) {
|
|
47
|
-
hooks?.onError?.(err
|
|
47
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
48
|
+
method: "chat",
|
|
49
|
+
request: req
|
|
50
|
+
});
|
|
48
51
|
throw err;
|
|
49
52
|
}
|
|
50
53
|
},
|
|
@@ -58,7 +61,10 @@ function createAI(adapter, options) {
|
|
|
58
61
|
try {
|
|
59
62
|
yield* source.call(adapter, req);
|
|
60
63
|
} catch (err) {
|
|
61
|
-
hooks?.onError?.(err
|
|
64
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
65
|
+
method: "stream",
|
|
66
|
+
request: req
|
|
67
|
+
});
|
|
62
68
|
throw err;
|
|
63
69
|
}
|
|
64
70
|
};
|
|
@@ -71,12 +77,7 @@ function createAI(adapter, options) {
|
|
|
71
77
|
return adapter.embeddings(request);
|
|
72
78
|
},
|
|
73
79
|
async run(options2) {
|
|
74
|
-
const {
|
|
75
|
-
tools,
|
|
76
|
-
maxRounds = 10,
|
|
77
|
-
onToolCall,
|
|
78
|
-
...requestBase
|
|
79
|
-
} = options2;
|
|
80
|
+
const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
|
|
80
81
|
const messages = [...options2.messages];
|
|
81
82
|
const toolDefs = tools.map((t) => t.definition);
|
|
82
83
|
const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
|
|
@@ -95,7 +96,10 @@ function createAI(adapter, options) {
|
|
|
95
96
|
try {
|
|
96
97
|
response = await withRetry(() => adapter.chat(req2), retry);
|
|
97
98
|
} catch (err) {
|
|
98
|
-
hooks?.onError?.(err
|
|
99
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
100
|
+
method: "run",
|
|
101
|
+
request: req2
|
|
102
|
+
});
|
|
99
103
|
throw err;
|
|
100
104
|
}
|
|
101
105
|
if (hooks?.onAfterChat) {
|
|
@@ -124,19 +128,21 @@ function createAI(adapter, options) {
|
|
|
124
128
|
try {
|
|
125
129
|
result = await handler(call.arguments);
|
|
126
130
|
} catch (err) {
|
|
127
|
-
result = err.message
|
|
131
|
+
result = err instanceof Error ? err.message : String(err);
|
|
128
132
|
isError = true;
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
onToolCall?.(call, result);
|
|
132
136
|
messages.push({
|
|
133
137
|
role: "tool",
|
|
134
|
-
content: [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "tool_result",
|
|
141
|
+
id: call.id,
|
|
142
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
143
|
+
isError
|
|
144
|
+
}
|
|
145
|
+
]
|
|
140
146
|
});
|
|
141
147
|
}
|
|
142
148
|
}
|
|
@@ -154,7 +160,10 @@ function createAI(adapter, options) {
|
|
|
154
160
|
try {
|
|
155
161
|
finalResponse = await withRetry(() => adapter.chat(req), retry);
|
|
156
162
|
} catch (err) {
|
|
157
|
-
hooks?.onError?.(err
|
|
163
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
164
|
+
method: "run",
|
|
165
|
+
request: req
|
|
166
|
+
});
|
|
158
167
|
throw err;
|
|
159
168
|
}
|
|
160
169
|
if (hooks?.onAfterChat) {
|
|
@@ -173,8 +182,8 @@ async function withRetry(fn, config) {
|
|
|
173
182
|
try {
|
|
174
183
|
return await fn();
|
|
175
184
|
} catch (err) {
|
|
176
|
-
lastError = err;
|
|
177
|
-
if (attempt < maxRetries && shouldRetry(
|
|
185
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
186
|
+
if (attempt < maxRetries && shouldRetry(lastError)) {
|
|
178
187
|
await sleep(baseDelay * Math.pow(2, attempt));
|
|
179
188
|
continue;
|
|
180
189
|
}
|
|
@@ -204,7 +213,7 @@ async function streamSSE(res, stream, options) {
|
|
|
204
213
|
res.writeHead(200, {
|
|
205
214
|
"Content-Type": "text/event-stream",
|
|
206
215
|
"Cache-Control": "no-cache",
|
|
207
|
-
|
|
216
|
+
Connection: "keep-alive",
|
|
208
217
|
"X-Accel-Buffering": "no"
|
|
209
218
|
});
|
|
210
219
|
try {
|
|
@@ -227,6 +236,9 @@ async function collectStream(stream) {
|
|
|
227
236
|
return result;
|
|
228
237
|
}
|
|
229
238
|
async function* parseSSEStream(response) {
|
|
239
|
+
if (!response.body) {
|
|
240
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
241
|
+
}
|
|
230
242
|
const reader = response.body.getReader();
|
|
231
243
|
const decoder = new TextDecoder();
|
|
232
244
|
let buffer = "";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,10 @@ function createAI(adapter, options) {
|
|
|
15
15
|
}
|
|
16
16
|
return response;
|
|
17
17
|
} catch (err) {
|
|
18
|
-
hooks?.onError?.(err
|
|
18
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
19
|
+
method: "chat",
|
|
20
|
+
request: req
|
|
21
|
+
});
|
|
19
22
|
throw err;
|
|
20
23
|
}
|
|
21
24
|
},
|
|
@@ -29,7 +32,10 @@ function createAI(adapter, options) {
|
|
|
29
32
|
try {
|
|
30
33
|
yield* source.call(adapter, req);
|
|
31
34
|
} catch (err) {
|
|
32
|
-
hooks?.onError?.(err
|
|
35
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
36
|
+
method: "stream",
|
|
37
|
+
request: req
|
|
38
|
+
});
|
|
33
39
|
throw err;
|
|
34
40
|
}
|
|
35
41
|
};
|
|
@@ -42,12 +48,7 @@ function createAI(adapter, options) {
|
|
|
42
48
|
return adapter.embeddings(request);
|
|
43
49
|
},
|
|
44
50
|
async run(options2) {
|
|
45
|
-
const {
|
|
46
|
-
tools,
|
|
47
|
-
maxRounds = 10,
|
|
48
|
-
onToolCall,
|
|
49
|
-
...requestBase
|
|
50
|
-
} = options2;
|
|
51
|
+
const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
|
|
51
52
|
const messages = [...options2.messages];
|
|
52
53
|
const toolDefs = tools.map((t) => t.definition);
|
|
53
54
|
const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
|
|
@@ -66,7 +67,10 @@ function createAI(adapter, options) {
|
|
|
66
67
|
try {
|
|
67
68
|
response = await withRetry(() => adapter.chat(req2), retry);
|
|
68
69
|
} catch (err) {
|
|
69
|
-
hooks?.onError?.(err
|
|
70
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
71
|
+
method: "run",
|
|
72
|
+
request: req2
|
|
73
|
+
});
|
|
70
74
|
throw err;
|
|
71
75
|
}
|
|
72
76
|
if (hooks?.onAfterChat) {
|
|
@@ -95,19 +99,21 @@ function createAI(adapter, options) {
|
|
|
95
99
|
try {
|
|
96
100
|
result = await handler(call.arguments);
|
|
97
101
|
} catch (err) {
|
|
98
|
-
result = err.message
|
|
102
|
+
result = err instanceof Error ? err.message : String(err);
|
|
99
103
|
isError = true;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
onToolCall?.(call, result);
|
|
103
107
|
messages.push({
|
|
104
108
|
role: "tool",
|
|
105
|
-
content: [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "tool_result",
|
|
112
|
+
id: call.id,
|
|
113
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
114
|
+
isError
|
|
115
|
+
}
|
|
116
|
+
]
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -125,7 +131,10 @@ function createAI(adapter, options) {
|
|
|
125
131
|
try {
|
|
126
132
|
finalResponse = await withRetry(() => adapter.chat(req), retry);
|
|
127
133
|
} catch (err) {
|
|
128
|
-
hooks?.onError?.(err
|
|
134
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
135
|
+
method: "run",
|
|
136
|
+
request: req
|
|
137
|
+
});
|
|
129
138
|
throw err;
|
|
130
139
|
}
|
|
131
140
|
if (hooks?.onAfterChat) {
|
|
@@ -144,8 +153,8 @@ async function withRetry(fn, config) {
|
|
|
144
153
|
try {
|
|
145
154
|
return await fn();
|
|
146
155
|
} catch (err) {
|
|
147
|
-
lastError = err;
|
|
148
|
-
if (attempt < maxRetries && shouldRetry(
|
|
156
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
157
|
+
if (attempt < maxRetries && shouldRetry(lastError)) {
|
|
149
158
|
await sleep(baseDelay * Math.pow(2, attempt));
|
|
150
159
|
continue;
|
|
151
160
|
}
|
|
@@ -175,7 +184,7 @@ async function streamSSE(res, stream, options) {
|
|
|
175
184
|
res.writeHead(200, {
|
|
176
185
|
"Content-Type": "text/event-stream",
|
|
177
186
|
"Cache-Control": "no-cache",
|
|
178
|
-
|
|
187
|
+
Connection: "keep-alive",
|
|
179
188
|
"X-Accel-Buffering": "no"
|
|
180
189
|
});
|
|
181
190
|
try {
|
|
@@ -198,6 +207,9 @@ async function collectStream(stream) {
|
|
|
198
207
|
return result;
|
|
199
208
|
}
|
|
200
209
|
async function* parseSSEStream(response) {
|
|
210
|
+
if (!response.body) {
|
|
211
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
212
|
+
}
|
|
201
213
|
const reader = response.body.getReader();
|
|
202
214
|
const decoder = new TextDecoder();
|
|
203
215
|
let buffer = "";
|
package/dist/mcp/index.cjs
CHANGED
|
@@ -47,13 +47,14 @@ function createMCPServer(config) {
|
|
|
47
47
|
const result = await dispatch(request);
|
|
48
48
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
49
49
|
} catch (err) {
|
|
50
|
+
const rpcErr = err;
|
|
50
51
|
return {
|
|
51
52
|
jsonrpc: "2.0",
|
|
52
53
|
id: request.id,
|
|
53
54
|
error: {
|
|
54
|
-
code:
|
|
55
|
-
message:
|
|
56
|
-
data:
|
|
55
|
+
code: rpcErr.code ?? -32603,
|
|
56
|
+
message: rpcErr.message ?? "Internal error",
|
|
57
|
+
data: rpcErr.data
|
|
57
58
|
}
|
|
58
59
|
};
|
|
59
60
|
}
|
|
@@ -172,29 +173,28 @@ function createMCPClient(config) {
|
|
|
172
173
|
}
|
|
173
174
|
const result = await response.json();
|
|
174
175
|
if (result.error) {
|
|
175
|
-
const err = new Error(result.error.message)
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const err = Object.assign(new Error(result.error.message), {
|
|
177
|
+
code: result.error.code,
|
|
178
|
+
data: result.error.data
|
|
179
|
+
});
|
|
178
180
|
throw err;
|
|
179
181
|
}
|
|
180
182
|
return result.result;
|
|
181
183
|
}
|
|
182
184
|
return {
|
|
183
185
|
async initialize() {
|
|
184
|
-
|
|
186
|
+
return rpc("initialize", {
|
|
185
187
|
protocolVersion: "2024-11-05",
|
|
186
188
|
capabilities: {},
|
|
187
189
|
clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
|
|
188
190
|
});
|
|
189
|
-
return result;
|
|
190
191
|
},
|
|
191
192
|
async listTools() {
|
|
192
193
|
const result = await rpc("tools/list");
|
|
193
194
|
return result.tools ?? [];
|
|
194
195
|
},
|
|
195
196
|
async callTool(name, args) {
|
|
196
|
-
|
|
197
|
-
return result;
|
|
197
|
+
return rpc("tools/call", { name, arguments: args });
|
|
198
198
|
},
|
|
199
199
|
async listResources() {
|
|
200
200
|
const result = await rpc("resources/list");
|
|
@@ -209,27 +209,28 @@ function createMCPClient(config) {
|
|
|
209
209
|
return result.prompts ?? [];
|
|
210
210
|
},
|
|
211
211
|
async getPrompt(name, args) {
|
|
212
|
-
|
|
213
|
-
return result;
|
|
212
|
+
return rpc("prompts/get", { name, arguments: args });
|
|
214
213
|
},
|
|
215
214
|
async toToolHandlers() {
|
|
216
215
|
const tools = await this.listTools();
|
|
217
|
-
return tools.map(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
216
|
+
return tools.map(
|
|
217
|
+
(tool) => ({
|
|
218
|
+
definition: {
|
|
219
|
+
name: tool.name,
|
|
220
|
+
description: tool.description,
|
|
221
|
+
parameters: tool.inputSchema
|
|
222
|
+
},
|
|
223
|
+
handler: async (args) => {
|
|
224
|
+
const result = await this.callTool(tool.name, args);
|
|
225
|
+
if (result.isError) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return result.content.map((c) => c.text ?? "").join("\n");
|
|
229
231
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}));
|
|
232
|
+
})
|
|
233
|
+
);
|
|
233
234
|
}
|
|
234
235
|
};
|
|
235
236
|
}
|
|
@@ -267,18 +268,24 @@ async function handlePost(server, req, res) {
|
|
|
267
268
|
res.end(JSON.stringify(response));
|
|
268
269
|
} catch (err) {
|
|
269
270
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
270
|
-
res.end(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
res.end(
|
|
272
|
+
JSON.stringify({
|
|
273
|
+
jsonrpc: "2.0",
|
|
274
|
+
id: null,
|
|
275
|
+
error: {
|
|
276
|
+
code: -32700,
|
|
277
|
+
message: "Parse error",
|
|
278
|
+
data: err instanceof Error ? err.message : String(err)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
);
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
function handleSSE(server, _req, res) {
|
|
278
285
|
res.writeHead(200, {
|
|
279
286
|
"Content-Type": "text/event-stream",
|
|
280
287
|
"Cache-Control": "no-cache",
|
|
281
|
-
|
|
288
|
+
Connection: "keep-alive"
|
|
282
289
|
});
|
|
283
290
|
const endpointUrl = "./";
|
|
284
291
|
res.write(`event: endpoint
|
package/dist/mcp/index.d.cts
CHANGED
|
@@ -170,27 +170,6 @@ interface MCPClient {
|
|
|
170
170
|
*/
|
|
171
171
|
toToolHandlers(): Promise<ToolHandler[]>;
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Create an MCP client that connects to an MCP server over HTTP.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
|
|
178
|
-
*
|
|
179
|
-
* const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
|
|
180
|
-
* await client.initialize();
|
|
181
|
-
*
|
|
182
|
-
* const tools = await client.listTools();
|
|
183
|
-
* const result = await client.callTool('get_weather', { city: 'NYC' });
|
|
184
|
-
*
|
|
185
|
-
* // Bridge MCP tools into AI tool loop
|
|
186
|
-
* const ai = createAI(openaiAdapter({ apiKey: '...' }));
|
|
187
|
-
* const toolHandlers = await client.toToolHandlers();
|
|
188
|
-
* const response = await ai.run({
|
|
189
|
-
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
190
|
-
* tools: toolHandlers,
|
|
191
|
-
* });
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
173
|
declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
195
174
|
|
|
196
175
|
/**
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -170,27 +170,6 @@ interface MCPClient {
|
|
|
170
170
|
*/
|
|
171
171
|
toToolHandlers(): Promise<ToolHandler[]>;
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Create an MCP client that connects to an MCP server over HTTP.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
|
|
178
|
-
*
|
|
179
|
-
* const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
|
|
180
|
-
* await client.initialize();
|
|
181
|
-
*
|
|
182
|
-
* const tools = await client.listTools();
|
|
183
|
-
* const result = await client.callTool('get_weather', { city: 'NYC' });
|
|
184
|
-
*
|
|
185
|
-
* // Bridge MCP tools into AI tool loop
|
|
186
|
-
* const ai = createAI(openaiAdapter({ apiKey: '...' }));
|
|
187
|
-
* const toolHandlers = await client.toToolHandlers();
|
|
188
|
-
* const response = await ai.run({
|
|
189
|
-
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
190
|
-
* tools: toolHandlers,
|
|
191
|
-
* });
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
173
|
declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
195
174
|
|
|
196
175
|
/**
|
package/dist/mcp/index.js
CHANGED
|
@@ -19,13 +19,14 @@ function createMCPServer(config) {
|
|
|
19
19
|
const result = await dispatch(request);
|
|
20
20
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
21
21
|
} catch (err) {
|
|
22
|
+
const rpcErr = err;
|
|
22
23
|
return {
|
|
23
24
|
jsonrpc: "2.0",
|
|
24
25
|
id: request.id,
|
|
25
26
|
error: {
|
|
26
|
-
code:
|
|
27
|
-
message:
|
|
28
|
-
data:
|
|
27
|
+
code: rpcErr.code ?? -32603,
|
|
28
|
+
message: rpcErr.message ?? "Internal error",
|
|
29
|
+
data: rpcErr.data
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -144,29 +145,28 @@ function createMCPClient(config) {
|
|
|
144
145
|
}
|
|
145
146
|
const result = await response.json();
|
|
146
147
|
if (result.error) {
|
|
147
|
-
const err = new Error(result.error.message)
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const err = Object.assign(new Error(result.error.message), {
|
|
149
|
+
code: result.error.code,
|
|
150
|
+
data: result.error.data
|
|
151
|
+
});
|
|
150
152
|
throw err;
|
|
151
153
|
}
|
|
152
154
|
return result.result;
|
|
153
155
|
}
|
|
154
156
|
return {
|
|
155
157
|
async initialize() {
|
|
156
|
-
|
|
158
|
+
return rpc("initialize", {
|
|
157
159
|
protocolVersion: "2024-11-05",
|
|
158
160
|
capabilities: {},
|
|
159
161
|
clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
|
|
160
162
|
});
|
|
161
|
-
return result;
|
|
162
163
|
},
|
|
163
164
|
async listTools() {
|
|
164
165
|
const result = await rpc("tools/list");
|
|
165
166
|
return result.tools ?? [];
|
|
166
167
|
},
|
|
167
168
|
async callTool(name, args) {
|
|
168
|
-
|
|
169
|
-
return result;
|
|
169
|
+
return rpc("tools/call", { name, arguments: args });
|
|
170
170
|
},
|
|
171
171
|
async listResources() {
|
|
172
172
|
const result = await rpc("resources/list");
|
|
@@ -181,27 +181,28 @@ function createMCPClient(config) {
|
|
|
181
181
|
return result.prompts ?? [];
|
|
182
182
|
},
|
|
183
183
|
async getPrompt(name, args) {
|
|
184
|
-
|
|
185
|
-
return result;
|
|
184
|
+
return rpc("prompts/get", { name, arguments: args });
|
|
186
185
|
},
|
|
187
186
|
async toToolHandlers() {
|
|
188
187
|
const tools = await this.listTools();
|
|
189
|
-
return tools.map(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
return tools.map(
|
|
189
|
+
(tool) => ({
|
|
190
|
+
definition: {
|
|
191
|
+
name: tool.name,
|
|
192
|
+
description: tool.description,
|
|
193
|
+
parameters: tool.inputSchema
|
|
194
|
+
},
|
|
195
|
+
handler: async (args) => {
|
|
196
|
+
const result = await this.callTool(tool.name, args);
|
|
197
|
+
if (result.isError) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return result.content.map((c) => c.text ?? "").join("\n");
|
|
201
203
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}));
|
|
204
|
+
})
|
|
205
|
+
);
|
|
205
206
|
}
|
|
206
207
|
};
|
|
207
208
|
}
|
|
@@ -239,18 +240,24 @@ async function handlePost(server, req, res) {
|
|
|
239
240
|
res.end(JSON.stringify(response));
|
|
240
241
|
} catch (err) {
|
|
241
242
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
242
|
-
res.end(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
res.end(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: null,
|
|
247
|
+
error: {
|
|
248
|
+
code: -32700,
|
|
249
|
+
message: "Parse error",
|
|
250
|
+
data: err instanceof Error ? err.message : String(err)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
);
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
function handleSSE(server, _req, res) {
|
|
250
257
|
res.writeHead(200, {
|
|
251
258
|
"Content-Type": "text/event-stream",
|
|
252
259
|
"Cache-Control": "no-cache",
|
|
253
|
-
|
|
260
|
+
Connection: "keep-alive"
|
|
254
261
|
});
|
|
255
262
|
const endpointUrl = "./";
|
|
256
263
|
res.write(`event: endpoint
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI adapters and MCP support for UtopiaJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"dist"
|
|
68
68
|
],
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@anthropic-ai/sdk": "^0.30.0",
|
|
71
|
-
"@google/generative-ai": "^0.21.0",
|
|
72
|
-
"openai": "^4.0.0"
|
|
70
|
+
"@anthropic-ai/sdk": "^0.30.0 || ^0.74.0",
|
|
71
|
+
"@google/generative-ai": "^0.21.0 || ^0.24.0",
|
|
72
|
+
"openai": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|
|
75
75
|
"openai": {
|