@matthesketh/utopia-ai 0.0.5 → 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.
@@ -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 Anthropic;
40
+ let AnthropicCtor;
41
41
  try {
42
42
  const mod = await import("@anthropic-ai/sdk");
43
- Anthropic = mod.Anthropic ?? mod.default;
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 Anthropic({
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(body);
77
- const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
78
- const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({
79
- id: b.id,
80
- name: b.name,
81
- arguments: b.input ?? {}
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(body);
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 Anthropic;
6
+ let AnthropicCtor;
7
7
  try {
8
8
  const mod = await import("@anthropic-ai/sdk");
9
- Anthropic = mod.Anthropic ?? mod.default;
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 Anthropic({
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(body);
43
- const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
44
- const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({
45
- id: b.id,
46
- name: b.name,
47
- arguments: b.input ?? {}
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(body);
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,
@@ -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
- id: `call_${crypto.randomUUID()}`,
79
- name: p.functionCall.name,
80
- arguments: p.functionCall.args ?? {}
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((text) => ({
136
- content: { parts: [{ text }] }
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),
@@ -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
- id: `call_${crypto.randomUUID()}`,
45
- name: p.functionCall.name,
46
- arguments: p.functionCall.args ?? {}
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((text) => ({
102
- content: { parts: [{ text }] }
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),
@@ -23,41 +23,42 @@ __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
  }
44
46
  const response = await fetch(`${baseURL}/api/chat`, {
45
47
  method: "POST",
46
48
  headers: { "Content-Type": "application/json" },
47
- body: JSON.stringify(body)
49
+ body: JSON.stringify(body),
50
+ signal: AbortSignal.timeout(6e4)
48
51
  });
49
52
  if (!response.ok) {
50
53
  const text = await response.text();
51
54
  throw new Error(`Ollama error ${response.status}: ${text}`);
52
55
  }
53
56
  const data = await response.json();
54
- const toolCalls = (data.message?.tool_calls ?? []).map(
55
- (tc, i) => ({
56
- id: `call_${i}`,
57
- name: tc.function.name,
58
- arguments: tc.function.arguments ?? {}
59
- })
60
- );
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
+ }));
61
62
  return {
62
63
  content: data.message?.content ?? "",
63
64
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -72,28 +73,33 @@ function ollamaAdapter(config = {}) {
72
73
  },
73
74
  async *stream(request) {
74
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;
75
81
  const body = {
76
82
  model,
77
83
  messages: toOllamaMessages(request.messages),
78
84
  stream: true,
79
- options: {}
85
+ options: streamOptions
80
86
  };
81
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
82
- if (request.topP !== void 0) body.options.top_p = request.topP;
83
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
84
- if (request.stop) body.options.stop = request.stop;
85
87
  if (request.tools?.length) {
86
88
  body.tools = request.tools.map(toOllamaTool);
87
89
  }
88
90
  const response = await fetch(`${baseURL}/api/chat`, {
89
91
  method: "POST",
90
92
  headers: { "Content-Type": "application/json" },
91
- body: JSON.stringify(body)
93
+ body: JSON.stringify(body),
94
+ signal: AbortSignal.timeout(6e4)
92
95
  });
93
96
  if (!response.ok) {
94
97
  const text = await response.text();
95
98
  throw new Error(`Ollama error ${response.status}: ${text}`);
96
99
  }
100
+ if (!response.body) {
101
+ throw new Error("Response body is null \u2014 streaming not supported");
102
+ }
97
103
  const reader = response.body.getReader();
98
104
  const decoder = new TextDecoder();
99
105
  let buffer = "";
@@ -105,7 +111,12 @@ function ollamaAdapter(config = {}) {
105
111
  buffer = lines.pop() ?? "";
106
112
  for (const line of lines) {
107
113
  if (!line.trim()) continue;
108
- const data = JSON.parse(line);
114
+ let data;
115
+ try {
116
+ data = JSON.parse(line);
117
+ } catch {
118
+ continue;
119
+ }
109
120
  yield {
110
121
  delta: data.message?.content ?? "",
111
122
  finishReason: data.done ? "stop" : void 0,
@@ -1,39 +1,40 @@
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
  }
20
22
  const response = await fetch(`${baseURL}/api/chat`, {
21
23
  method: "POST",
22
24
  headers: { "Content-Type": "application/json" },
23
- body: JSON.stringify(body)
25
+ body: JSON.stringify(body),
26
+ signal: AbortSignal.timeout(6e4)
24
27
  });
25
28
  if (!response.ok) {
26
29
  const text = await response.text();
27
30
  throw new Error(`Ollama error ${response.status}: ${text}`);
28
31
  }
29
32
  const data = await response.json();
30
- const toolCalls = (data.message?.tool_calls ?? []).map(
31
- (tc, i) => ({
32
- id: `call_${i}`,
33
- name: tc.function.name,
34
- arguments: tc.function.arguments ?? {}
35
- })
36
- );
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
+ }));
37
38
  return {
38
39
  content: data.message?.content ?? "",
39
40
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -48,28 +49,33 @@ function ollamaAdapter(config = {}) {
48
49
  },
49
50
  async *stream(request) {
50
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;
51
57
  const body = {
52
58
  model,
53
59
  messages: toOllamaMessages(request.messages),
54
60
  stream: true,
55
- options: {}
61
+ options: streamOptions
56
62
  };
57
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
58
- if (request.topP !== void 0) body.options.top_p = request.topP;
59
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
60
- if (request.stop) body.options.stop = request.stop;
61
63
  if (request.tools?.length) {
62
64
  body.tools = request.tools.map(toOllamaTool);
63
65
  }
64
66
  const response = await fetch(`${baseURL}/api/chat`, {
65
67
  method: "POST",
66
68
  headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify(body)
69
+ body: JSON.stringify(body),
70
+ signal: AbortSignal.timeout(6e4)
68
71
  });
69
72
  if (!response.ok) {
70
73
  const text = await response.text();
71
74
  throw new Error(`Ollama error ${response.status}: ${text}`);
72
75
  }
76
+ if (!response.body) {
77
+ throw new Error("Response body is null \u2014 streaming not supported");
78
+ }
73
79
  const reader = response.body.getReader();
74
80
  const decoder = new TextDecoder();
75
81
  let buffer = "";
@@ -81,7 +87,12 @@ function ollamaAdapter(config = {}) {
81
87
  buffer = lines.pop() ?? "";
82
88
  for (const line of lines) {
83
89
  if (!line.trim()) continue;
84
- const data = JSON.parse(line);
90
+ let data;
91
+ try {
92
+ data = JSON.parse(line);
93
+ } catch {
94
+ continue;
95
+ }
85
96
  yield {
86
97
  delta: data.message?.content ?? "",
87
98
  finishReason: data.done ? "stop" : void 0,
@@ -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 OpenAI;
40
+ let OpenAICtor;
41
41
  try {
42
42
  const mod = await import("openai");
43
- OpenAI = mod.OpenAI ?? mod.default;
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 OpenAI({
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(body);
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(fromOpenAIToolCall),
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(body);
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 choice;
232
+ return void 0;
222
233
  }
223
234
  function fromOpenAIToolCall(tc) {
224
235
  return {
@@ -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 OpenAI;
6
+ let OpenAICtor;
7
7
  try {
8
8
  const mod = await import("openai");
9
- OpenAI = mod.OpenAI ?? mod.default;
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 OpenAI({
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(body);
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(fromOpenAIToolCall),
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(body);
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 choice;
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, { method: "chat", request: req });
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, { method: "stream", request: req });
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, { method: "run", request: req2 });
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 ?? String(err);
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
- type: "tool_result",
136
- id: call.id,
137
- content: typeof result === "string" ? result : JSON.stringify(result),
138
- isError
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, { method: "run", request: req });
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(err)) {
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,17 +213,20 @@ 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
- "Connection": "keep-alive",
216
+ Connection: "keep-alive",
208
217
  "X-Accel-Buffering": "no"
209
218
  });
210
- for await (const chunk of stream) {
211
- options?.onChunk?.(chunk);
212
- res.write(`data: ${JSON.stringify(chunk)}
219
+ try {
220
+ for await (const chunk of stream) {
221
+ options?.onChunk?.(chunk);
222
+ res.write(`data: ${JSON.stringify(chunk)}
213
223
 
214
224
  `);
225
+ }
226
+ res.write("data: [DONE]\n\n");
227
+ } finally {
228
+ res.end();
215
229
  }
216
- res.write("data: [DONE]\n\n");
217
- res.end();
218
230
  }
219
231
  async function collectStream(stream) {
220
232
  let result = "";
@@ -224,25 +236,33 @@ async function collectStream(stream) {
224
236
  return result;
225
237
  }
226
238
  async function* parseSSEStream(response) {
239
+ if (!response.body) {
240
+ throw new Error("Response body is null \u2014 streaming not supported");
241
+ }
227
242
  const reader = response.body.getReader();
228
243
  const decoder = new TextDecoder();
229
244
  let buffer = "";
230
- while (true) {
231
- const { done, value } = await reader.read();
232
- if (done) break;
233
- buffer += decoder.decode(value, { stream: true });
234
- const lines = buffer.split("\n");
235
- buffer = lines.pop() ?? "";
236
- for (const line of lines) {
237
- if (line.startsWith("data: ")) {
238
- const data = line.slice(6).trim();
239
- if (data === "[DONE]") return;
240
- try {
241
- yield JSON.parse(data);
242
- } catch {
245
+ try {
246
+ while (true) {
247
+ const { done, value } = await reader.read();
248
+ if (done) break;
249
+ buffer += decoder.decode(value, { stream: true });
250
+ const lines = buffer.split("\n");
251
+ buffer = lines.pop() ?? "";
252
+ for (const line of lines) {
253
+ if (line.startsWith("data: ")) {
254
+ const data = line.slice(6).trim();
255
+ if (data === "[DONE]") return;
256
+ try {
257
+ yield JSON.parse(data);
258
+ } catch {
259
+ }
243
260
  }
244
261
  }
245
262
  }
263
+ } finally {
264
+ reader.cancel().catch(() => {
265
+ });
246
266
  }
247
267
  }
248
268
  // Annotate the CommonJS export names for ESM import in node:
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, { method: "chat", request: req });
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, { method: "stream", request: req });
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, { method: "run", request: req2 });
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 ?? String(err);
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
- type: "tool_result",
107
- id: call.id,
108
- content: typeof result === "string" ? result : JSON.stringify(result),
109
- isError
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, { method: "run", request: req });
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(err)) {
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,17 +184,20 @@ 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
- "Connection": "keep-alive",
187
+ Connection: "keep-alive",
179
188
  "X-Accel-Buffering": "no"
180
189
  });
181
- for await (const chunk of stream) {
182
- options?.onChunk?.(chunk);
183
- res.write(`data: ${JSON.stringify(chunk)}
190
+ try {
191
+ for await (const chunk of stream) {
192
+ options?.onChunk?.(chunk);
193
+ res.write(`data: ${JSON.stringify(chunk)}
184
194
 
185
195
  `);
196
+ }
197
+ res.write("data: [DONE]\n\n");
198
+ } finally {
199
+ res.end();
186
200
  }
187
- res.write("data: [DONE]\n\n");
188
- res.end();
189
201
  }
190
202
  async function collectStream(stream) {
191
203
  let result = "";
@@ -195,25 +207,33 @@ async function collectStream(stream) {
195
207
  return result;
196
208
  }
197
209
  async function* parseSSEStream(response) {
210
+ if (!response.body) {
211
+ throw new Error("Response body is null \u2014 streaming not supported");
212
+ }
198
213
  const reader = response.body.getReader();
199
214
  const decoder = new TextDecoder();
200
215
  let buffer = "";
201
- while (true) {
202
- const { done, value } = await reader.read();
203
- if (done) break;
204
- buffer += decoder.decode(value, { stream: true });
205
- const lines = buffer.split("\n");
206
- buffer = lines.pop() ?? "";
207
- for (const line of lines) {
208
- if (line.startsWith("data: ")) {
209
- const data = line.slice(6).trim();
210
- if (data === "[DONE]") return;
211
- try {
212
- yield JSON.parse(data);
213
- } catch {
216
+ try {
217
+ while (true) {
218
+ const { done, value } = await reader.read();
219
+ if (done) break;
220
+ buffer += decoder.decode(value, { stream: true });
221
+ const lines = buffer.split("\n");
222
+ buffer = lines.pop() ?? "";
223
+ for (const line of lines) {
224
+ if (line.startsWith("data: ")) {
225
+ const data = line.slice(6).trim();
226
+ if (data === "[DONE]") return;
227
+ try {
228
+ yield JSON.parse(data);
229
+ } catch {
230
+ }
214
231
  }
215
232
  }
216
233
  }
234
+ } finally {
235
+ reader.cancel().catch(() => {
236
+ });
217
237
  }
218
238
  }
219
239
  export {
@@ -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: err.code ?? -32603,
55
- message: err.message ?? "Internal error",
56
- data: err.data
55
+ code: rpcErr.code ?? -32603,
56
+ message: rpcErr.message ?? "Internal error",
57
+ data: rpcErr.data
57
58
  }
58
59
  };
59
60
  }
@@ -139,7 +140,8 @@ function createMCPServer(config) {
139
140
  return { handleRequest, info: () => serverInfo };
140
141
  }
141
142
  function matchesTemplate(pattern, uri) {
142
- const regex = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
143
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
144
+ const regex = escaped.replace(/\\\{[^\\}]+\\\}/g, "([^/]+)");
143
145
  return new RegExp(`^${regex}$`).test(uri);
144
146
  }
145
147
  function makeError(code, message) {
@@ -162,7 +164,8 @@ function createMCPClient(config) {
162
164
  "Content-Type": "application/json",
163
165
  ...config.headers
164
166
  },
165
- body: JSON.stringify(request)
167
+ body: JSON.stringify(request),
168
+ signal: AbortSignal.timeout(3e4)
166
169
  });
167
170
  if (!response.ok) {
168
171
  const text = await response.text();
@@ -170,29 +173,28 @@ function createMCPClient(config) {
170
173
  }
171
174
  const result = await response.json();
172
175
  if (result.error) {
173
- const err = new Error(result.error.message);
174
- err.code = result.error.code;
175
- err.data = result.error.data;
176
+ const err = Object.assign(new Error(result.error.message), {
177
+ code: result.error.code,
178
+ data: result.error.data
179
+ });
176
180
  throw err;
177
181
  }
178
182
  return result.result;
179
183
  }
180
184
  return {
181
185
  async initialize() {
182
- const result = await rpc("initialize", {
186
+ return rpc("initialize", {
183
187
  protocolVersion: "2024-11-05",
184
188
  capabilities: {},
185
189
  clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
186
190
  });
187
- return result;
188
191
  },
189
192
  async listTools() {
190
193
  const result = await rpc("tools/list");
191
194
  return result.tools ?? [];
192
195
  },
193
196
  async callTool(name, args) {
194
- const result = await rpc("tools/call", { name, arguments: args });
195
- return result;
197
+ return rpc("tools/call", { name, arguments: args });
196
198
  },
197
199
  async listResources() {
198
200
  const result = await rpc("resources/list");
@@ -207,27 +209,28 @@ function createMCPClient(config) {
207
209
  return result.prompts ?? [];
208
210
  },
209
211
  async getPrompt(name, args) {
210
- const result = await rpc("prompts/get", { name, arguments: args });
211
- return result;
212
+ return rpc("prompts/get", { name, arguments: args });
212
213
  },
213
214
  async toToolHandlers() {
214
215
  const tools = await this.listTools();
215
- return tools.map((tool) => ({
216
- definition: {
217
- name: tool.name,
218
- description: tool.description,
219
- parameters: tool.inputSchema
220
- },
221
- handler: async (args) => {
222
- const result = await this.callTool(tool.name, args);
223
- if (result.isError) {
224
- throw new Error(
225
- result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
226
- );
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");
227
231
  }
228
- return result.content.map((c) => c.text ?? "").join("\n");
229
- }
230
- }));
232
+ })
233
+ );
231
234
  }
232
235
  };
233
236
  }
@@ -265,18 +268,24 @@ async function handlePost(server, req, res) {
265
268
  res.end(JSON.stringify(response));
266
269
  } catch (err) {
267
270
  res.writeHead(400, { "Content-Type": "application/json" });
268
- res.end(JSON.stringify({
269
- jsonrpc: "2.0",
270
- id: null,
271
- error: { code: -32700, message: "Parse error", data: err.message }
272
- }));
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
+ );
273
282
  }
274
283
  }
275
284
  function handleSSE(server, _req, res) {
276
285
  res.writeHead(200, {
277
286
  "Content-Type": "text/event-stream",
278
287
  "Cache-Control": "no-cache",
279
- "Connection": "keep-alive"
288
+ Connection: "keep-alive"
280
289
  });
281
290
  const endpointUrl = "./";
282
291
  res.write(`event: endpoint
@@ -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
  /**
@@ -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: err.code ?? -32603,
27
- message: err.message ?? "Internal error",
28
- data: err.data
27
+ code: rpcErr.code ?? -32603,
28
+ message: rpcErr.message ?? "Internal error",
29
+ data: rpcErr.data
29
30
  }
30
31
  };
31
32
  }
@@ -111,7 +112,8 @@ function createMCPServer(config) {
111
112
  return { handleRequest, info: () => serverInfo };
112
113
  }
113
114
  function matchesTemplate(pattern, uri) {
114
- const regex = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
115
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
116
+ const regex = escaped.replace(/\\\{[^\\}]+\\\}/g, "([^/]+)");
115
117
  return new RegExp(`^${regex}$`).test(uri);
116
118
  }
117
119
  function makeError(code, message) {
@@ -134,7 +136,8 @@ function createMCPClient(config) {
134
136
  "Content-Type": "application/json",
135
137
  ...config.headers
136
138
  },
137
- body: JSON.stringify(request)
139
+ body: JSON.stringify(request),
140
+ signal: AbortSignal.timeout(3e4)
138
141
  });
139
142
  if (!response.ok) {
140
143
  const text = await response.text();
@@ -142,29 +145,28 @@ function createMCPClient(config) {
142
145
  }
143
146
  const result = await response.json();
144
147
  if (result.error) {
145
- const err = new Error(result.error.message);
146
- err.code = result.error.code;
147
- err.data = result.error.data;
148
+ const err = Object.assign(new Error(result.error.message), {
149
+ code: result.error.code,
150
+ data: result.error.data
151
+ });
148
152
  throw err;
149
153
  }
150
154
  return result.result;
151
155
  }
152
156
  return {
153
157
  async initialize() {
154
- const result = await rpc("initialize", {
158
+ return rpc("initialize", {
155
159
  protocolVersion: "2024-11-05",
156
160
  capabilities: {},
157
161
  clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
158
162
  });
159
- return result;
160
163
  },
161
164
  async listTools() {
162
165
  const result = await rpc("tools/list");
163
166
  return result.tools ?? [];
164
167
  },
165
168
  async callTool(name, args) {
166
- const result = await rpc("tools/call", { name, arguments: args });
167
- return result;
169
+ return rpc("tools/call", { name, arguments: args });
168
170
  },
169
171
  async listResources() {
170
172
  const result = await rpc("resources/list");
@@ -179,27 +181,28 @@ function createMCPClient(config) {
179
181
  return result.prompts ?? [];
180
182
  },
181
183
  async getPrompt(name, args) {
182
- const result = await rpc("prompts/get", { name, arguments: args });
183
- return result;
184
+ return rpc("prompts/get", { name, arguments: args });
184
185
  },
185
186
  async toToolHandlers() {
186
187
  const tools = await this.listTools();
187
- return tools.map((tool) => ({
188
- definition: {
189
- name: tool.name,
190
- description: tool.description,
191
- parameters: tool.inputSchema
192
- },
193
- handler: async (args) => {
194
- const result = await this.callTool(tool.name, args);
195
- if (result.isError) {
196
- throw new Error(
197
- result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
198
- );
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");
199
203
  }
200
- return result.content.map((c) => c.text ?? "").join("\n");
201
- }
202
- }));
204
+ })
205
+ );
203
206
  }
204
207
  };
205
208
  }
@@ -237,18 +240,24 @@ async function handlePost(server, req, res) {
237
240
  res.end(JSON.stringify(response));
238
241
  } catch (err) {
239
242
  res.writeHead(400, { "Content-Type": "application/json" });
240
- res.end(JSON.stringify({
241
- jsonrpc: "2.0",
242
- id: null,
243
- error: { code: -32700, message: "Parse error", data: err.message }
244
- }));
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
+ );
245
254
  }
246
255
  }
247
256
  function handleSSE(server, _req, res) {
248
257
  res.writeHead(200, {
249
258
  "Content-Type": "text/event-stream",
250
259
  "Cache-Control": "no-cache",
251
- "Connection": "keep-alive"
260
+ Connection: "keep-alive"
252
261
  });
253
262
  const endpointUrl = "./";
254
263
  res.write(`event: endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-ai",
3
- "version": "0.0.5",
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": {