@matthesketh/utopia-ai 0.7.1 → 0.8.1

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.
@@ -46,6 +46,9 @@ function anthropicAdapter(config) {
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
+ if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
50
+ throw new Error(`Anthropic baseURL must be http(s): ${config.baseURL}`);
51
+ }
49
52
  client = new AnthropicCtor({
50
53
  apiKey: config.apiKey,
51
54
  ...config.baseURL ? { baseURL: config.baseURL } : {}
@@ -1,4 +1,4 @@
1
- import { b as AnthropicConfig, A as AIAdapter } from '../types-FSnS43LM.cjs';
1
+ import { b as AnthropicConfig, A as AIAdapter } from '../types-BcCKlL06.cjs';
2
2
 
3
3
  /**
4
4
  * Create an Anthropic adapter.
@@ -1,4 +1,4 @@
1
- import { b as AnthropicConfig, A as AIAdapter } from '../types-FSnS43LM.js';
1
+ import { b as AnthropicConfig, A as AIAdapter } from '../types-BcCKlL06.js';
2
2
 
3
3
  /**
4
4
  * Create an Anthropic adapter.
@@ -12,6 +12,9 @@ function anthropicAdapter(config) {
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
+ if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
16
+ throw new Error(`Anthropic baseURL must be http(s): ${config.baseURL}`);
17
+ }
15
18
  client = new AnthropicCtor({
16
19
  apiKey: config.apiKey,
17
20
  ...config.baseURL ? { baseURL: config.baseURL } : {}
@@ -1,4 +1,4 @@
1
- import { G as GoogleConfig, A as AIAdapter } from '../types-FSnS43LM.cjs';
1
+ import { G as GoogleConfig, A as AIAdapter } from '../types-BcCKlL06.cjs';
2
2
 
3
3
  /**
4
4
  * Create a Google Gemini adapter.
@@ -1,4 +1,4 @@
1
- import { G as GoogleConfig, A as AIAdapter } from '../types-FSnS43LM.js';
1
+ import { G as GoogleConfig, A as AIAdapter } from '../types-BcCKlL06.js';
2
2
 
3
3
  /**
4
4
  * Create a Google Gemini adapter.
@@ -25,9 +25,18 @@ __export(ollama_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(ollama_exports);
27
27
  var TRAILING_SLASH_RE = /\/$/;
28
+ var MAX_STREAM_BUFFER = 1024 * 1024;
29
+ function resolveBaseURL(raw) {
30
+ const baseURL = (raw ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
31
+ const protocol = new URL(baseURL).protocol;
32
+ if (protocol !== "http:" && protocol !== "https:") {
33
+ throw new Error(`Ollama baseURL must be http(s): ${baseURL}`);
34
+ }
35
+ return baseURL;
36
+ }
28
37
  var ollamaToolCallCounter = 0;
29
38
  function ollamaAdapter(config = {}) {
30
- const baseURL = (config.baseURL ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
39
+ const baseURL = resolveBaseURL(config.baseURL);
31
40
  return {
32
41
  async chat(request) {
33
42
  const model = request.model ?? config.defaultModel ?? "llama3.2";
@@ -109,6 +118,9 @@ function ollamaAdapter(config = {}) {
109
118
  const { done, value } = await reader.read();
110
119
  if (done) break;
111
120
  buffer += decoder.decode(value, { stream: true });
121
+ if (buffer.length > MAX_STREAM_BUFFER) {
122
+ throw new Error("Ollama stream exceeded maximum buffer size without a line delimiter");
123
+ }
112
124
  const lines = buffer.split("\n");
113
125
  buffer = lines.pop() ?? "";
114
126
  for (const line of lines) {
@@ -1,6 +1,6 @@
1
- import { O as OllamaConfig, A as AIAdapter } from '../types-FSnS43LM.cjs';
1
+ import { O as OllamaConfig, A as AIAdapter } from '../types-BcCKlL06.cjs';
2
2
 
3
- /** Matches a trailing slash for URL normalization. */
3
+ /** matches a trailing slash for URL normalization. */
4
4
  declare const TRAILING_SLASH_RE: RegExp;
5
5
  /**
6
6
  * Create an Ollama adapter for local models.
@@ -1,6 +1,6 @@
1
- import { O as OllamaConfig, A as AIAdapter } from '../types-FSnS43LM.js';
1
+ import { O as OllamaConfig, A as AIAdapter } from '../types-BcCKlL06.js';
2
2
 
3
- /** Matches a trailing slash for URL normalization. */
3
+ /** matches a trailing slash for URL normalization. */
4
4
  declare const TRAILING_SLASH_RE: RegExp;
5
5
  /**
6
6
  * Create an Ollama adapter for local models.
@@ -1,8 +1,17 @@
1
1
  // src/adapters/ollama.ts
2
2
  var TRAILING_SLASH_RE = /\/$/;
3
+ var MAX_STREAM_BUFFER = 1024 * 1024;
4
+ function resolveBaseURL(raw) {
5
+ const baseURL = (raw ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
6
+ const protocol = new URL(baseURL).protocol;
7
+ if (protocol !== "http:" && protocol !== "https:") {
8
+ throw new Error(`Ollama baseURL must be http(s): ${baseURL}`);
9
+ }
10
+ return baseURL;
11
+ }
3
12
  var ollamaToolCallCounter = 0;
4
13
  function ollamaAdapter(config = {}) {
5
- const baseURL = (config.baseURL ?? "http://localhost:11434").replace(TRAILING_SLASH_RE, "");
14
+ const baseURL = resolveBaseURL(config.baseURL);
6
15
  return {
7
16
  async chat(request) {
8
17
  const model = request.model ?? config.defaultModel ?? "llama3.2";
@@ -84,6 +93,9 @@ function ollamaAdapter(config = {}) {
84
93
  const { done, value } = await reader.read();
85
94
  if (done) break;
86
95
  buffer += decoder.decode(value, { stream: true });
96
+ if (buffer.length > MAX_STREAM_BUFFER) {
97
+ throw new Error("Ollama stream exceeded maximum buffer size without a line delimiter");
98
+ }
87
99
  const lines = buffer.split("\n");
88
100
  buffer = lines.pop() ?? "";
89
101
  for (const line of lines) {
@@ -46,6 +46,9 @@ function openaiAdapter(config) {
46
46
  '@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
47
47
  );
48
48
  }
49
+ if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
50
+ throw new Error(`OpenAI baseURL must be http(s): ${config.baseURL}`);
51
+ }
49
52
  client = new OpenAICtor({
50
53
  apiKey: config.apiKey,
51
54
  baseURL: config.baseURL,
@@ -1,4 +1,4 @@
1
- import { h as OpenAIConfig, A as AIAdapter } from '../types-FSnS43LM.cjs';
1
+ import { h as OpenAIConfig, A as AIAdapter } from '../types-BcCKlL06.cjs';
2
2
 
3
3
  /**
4
4
  * Create an OpenAI adapter.
@@ -1,4 +1,4 @@
1
- import { h as OpenAIConfig, A as AIAdapter } from '../types-FSnS43LM.js';
1
+ import { h as OpenAIConfig, A as AIAdapter } from '../types-BcCKlL06.js';
2
2
 
3
3
  /**
4
4
  * Create an OpenAI adapter.
@@ -12,6 +12,9 @@ function openaiAdapter(config) {
12
12
  '@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
13
13
  );
14
14
  }
15
+ if (config.baseURL && !/^https?:$/.test(new URL(config.baseURL).protocol)) {
16
+ throw new Error(`OpenAI baseURL must be http(s): ${config.baseURL}`);
17
+ }
15
18
  client = new OpenAICtor({
16
19
  apiKey: config.apiKey,
17
20
  baseURL: config.baseURL,
@@ -0,0 +1,66 @@
1
+ import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-BcCKlL06.js';
2
+
3
+ interface AI {
4
+ /** Send a chat completion request. */
5
+ chat(request: ChatRequest): Promise<ChatResponse>;
6
+ /** Stream a chat completion. */
7
+ stream(request: ChatRequest): AsyncIterable<ChatChunk>;
8
+ /** Generate embeddings. */
9
+ embeddings(request: EmbeddingRequest): Promise<EmbeddingResponse>;
10
+ /**
11
+ * Run a tool-calling loop: send messages, execute tool calls via the
12
+ * provided handlers, append results, and repeat until the model stops
13
+ * calling tools.
14
+ */
15
+ run(options: RunOptions): Promise<ChatResponse>;
16
+ }
17
+ interface ToolHandler {
18
+ definition: ToolDefinition;
19
+ handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
20
+ }
21
+ interface RunOptions {
22
+ messages: ChatMessage[];
23
+ tools: ToolHandler[];
24
+ model?: string;
25
+ temperature?: number;
26
+ maxTokens?: number;
27
+ maxRounds?: number;
28
+ onToolCall?: (call: ToolCall, result: unknown) => void;
29
+ extra?: Record<string, unknown>;
30
+ }
31
+ interface CreateAIOptions {
32
+ hooks?: AIHooks;
33
+ retry?: RetryConfig;
34
+ }
35
+ /**
36
+ * Create an AI instance with the given adapter.
37
+ *
38
+ * Usage:
39
+ * ```ts
40
+ * import { createAI } from '@matthesketh/utopia-ai';
41
+ * import { openaiAdapter } from '@matthesketh/utopia-ai/openai';
42
+ *
43
+ * const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY }));
44
+ *
45
+ * const res = await ai.chat({
46
+ * messages: [{ role: 'user', content: 'Hello!' }],
47
+ * });
48
+ *
49
+ * // Streaming
50
+ * for await (const chunk of ai.stream({ messages })) {
51
+ * process.stdout.write(chunk.delta);
52
+ * }
53
+ *
54
+ * // Agentic tool loop
55
+ * const result = await ai.run({
56
+ * messages: [{ role: 'user', content: 'What is the weather?' }],
57
+ * tools: [{
58
+ * definition: { name: 'get_weather', description: '...', parameters: { type: 'object', properties: {} } },
59
+ * handler: async ({ city }) => ({ temp: 72 }),
60
+ * }],
61
+ * });
62
+ * ```
63
+ */
64
+ declare function createAI(adapter: AIAdapter, options?: CreateAIOptions): AI;
65
+
66
+ export { type AI as A, type CreateAIOptions as C, type RunOptions as R, type ToolHandler as T, createAI as c };
@@ -1,4 +1,4 @@
1
- import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM.js';
1
+ import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM';
2
2
 
3
3
  interface AI {
4
4
  /** Send a chat completion request. */
@@ -0,0 +1,66 @@
1
+ import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-BcCKlL06.cjs';
2
+
3
+ interface AI {
4
+ /** Send a chat completion request. */
5
+ chat(request: ChatRequest): Promise<ChatResponse>;
6
+ /** Stream a chat completion. */
7
+ stream(request: ChatRequest): AsyncIterable<ChatChunk>;
8
+ /** Generate embeddings. */
9
+ embeddings(request: EmbeddingRequest): Promise<EmbeddingResponse>;
10
+ /**
11
+ * Run a tool-calling loop: send messages, execute tool calls via the
12
+ * provided handlers, append results, and repeat until the model stops
13
+ * calling tools.
14
+ */
15
+ run(options: RunOptions): Promise<ChatResponse>;
16
+ }
17
+ interface ToolHandler {
18
+ definition: ToolDefinition;
19
+ handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
20
+ }
21
+ interface RunOptions {
22
+ messages: ChatMessage[];
23
+ tools: ToolHandler[];
24
+ model?: string;
25
+ temperature?: number;
26
+ maxTokens?: number;
27
+ maxRounds?: number;
28
+ onToolCall?: (call: ToolCall, result: unknown) => void;
29
+ extra?: Record<string, unknown>;
30
+ }
31
+ interface CreateAIOptions {
32
+ hooks?: AIHooks;
33
+ retry?: RetryConfig;
34
+ }
35
+ /**
36
+ * Create an AI instance with the given adapter.
37
+ *
38
+ * Usage:
39
+ * ```ts
40
+ * import { createAI } from '@matthesketh/utopia-ai';
41
+ * import { openaiAdapter } from '@matthesketh/utopia-ai/openai';
42
+ *
43
+ * const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY }));
44
+ *
45
+ * const res = await ai.chat({
46
+ * messages: [{ role: 'user', content: 'Hello!' }],
47
+ * });
48
+ *
49
+ * // Streaming
50
+ * for await (const chunk of ai.stream({ messages })) {
51
+ * process.stdout.write(chunk.delta);
52
+ * }
53
+ *
54
+ * // Agentic tool loop
55
+ * const result = await ai.run({
56
+ * messages: [{ role: 'user', content: 'What is the weather?' }],
57
+ * tools: [{
58
+ * definition: { name: 'get_weather', description: '...', parameters: { type: 'object', properties: {} } },
59
+ * handler: async ({ city }) => ({ temp: 72 }),
60
+ * }],
61
+ * });
62
+ * ```
63
+ */
64
+ declare function createAI(adapter: AIAdapter, options?: CreateAIOptions): AI;
65
+
66
+ export { type AI as A, type CreateAIOptions as C, type RunOptions as R, type ToolHandler as T, createAI as c };
package/dist/index.cjs CHANGED
@@ -209,6 +209,7 @@ async function* chatToStream(adapter, request) {
209
209
  }
210
210
 
211
211
  // src/streaming.ts
212
+ var MAX_SSE_BUFFER = 1024 * 1024;
212
213
  async function streamSSE(res, stream, options) {
213
214
  res.writeHead(200, {
214
215
  "Content-Type": "text/event-stream",
@@ -247,6 +248,9 @@ async function* parseSSEStream(response) {
247
248
  const { done, value } = await reader.read();
248
249
  if (done) break;
249
250
  buffer += decoder.decode(value, { stream: true });
251
+ if (buffer.length > MAX_SSE_BUFFER) {
252
+ throw new Error("SSE stream exceeded maximum buffer size without a line delimiter");
253
+ }
250
254
  const lines = buffer.split("\n");
251
255
  buffer = lines.pop() ?? "";
252
256
  for (const line of lines) {
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-B2NkPypc.cjs';
1
+ export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-dyp9MfTz.cjs';
2
2
  import { ServerResponse } from 'node:http';
3
- import { C as ChatChunk } from './types-FSnS43LM.cjs';
4
- export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-FSnS43LM.cjs';
3
+ import { C as ChatChunk } from './types-BcCKlL06.cjs';
4
+ export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-BcCKlL06.cjs';
5
5
 
6
6
  /**
7
7
  * Stream AI chat chunks as Server-Sent Events (SSE).
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-VMRaOOak.js';
1
+ export { A as AI, C as CreateAIOptions, R as RunOptions, T as ToolHandler, c as createAI } from './ai-B65XFWfZ.js';
2
2
  import { ServerResponse } from 'node:http';
3
- import { C as ChatChunk } from './types-FSnS43LM.js';
4
- export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-FSnS43LM.js';
3
+ import { C as ChatChunk } from './types-BcCKlL06.js';
4
+ export { A as AIAdapter, a as AIHooks, b as AnthropicConfig, c as ChatMessage, d as ChatRequest, e as ChatResponse, E as EmbeddingRequest, f as EmbeddingResponse, G as GoogleConfig, I as ImageContent, J as JsonSchema, M as MessageContent, g as MessageRole, O as OllamaConfig, h as OpenAIConfig, R as RetryConfig, T as TextContent, i as TokenUsage, j as ToolCall, k as ToolCallContent, l as ToolDefinition, m as ToolResultContent } from './types-BcCKlL06.js';
5
5
 
6
6
  /**
7
7
  * Stream AI chat chunks as Server-Sent Events (SSE).
package/dist/index.js CHANGED
@@ -180,6 +180,7 @@ async function* chatToStream(adapter, request) {
180
180
  }
181
181
 
182
182
  // src/streaming.ts
183
+ var MAX_SSE_BUFFER = 1024 * 1024;
183
184
  async function streamSSE(res, stream, options) {
184
185
  res.writeHead(200, {
185
186
  "Content-Type": "text/event-stream",
@@ -218,6 +219,9 @@ async function* parseSSEStream(response) {
218
219
  const { done, value } = await reader.read();
219
220
  if (done) break;
220
221
  buffer += decoder.decode(value, { stream: true });
222
+ if (buffer.length > MAX_SSE_BUFFER) {
223
+ throw new Error("SSE stream exceeded maximum buffer size without a line delimiter");
224
+ }
221
225
  const lines = buffer.split("\n");
222
226
  buffer = lines.pop() ?? "";
223
227
  for (const line of lines) {
@@ -58,14 +58,17 @@ function createMCPServer(config) {
58
58
  return { jsonrpc: "2.0", id: request.id, result };
59
59
  } catch (err) {
60
60
  const rpcErr = err;
61
+ if (typeof rpcErr.code === "number") {
62
+ return {
63
+ jsonrpc: "2.0",
64
+ id: request.id,
65
+ error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
66
+ };
67
+ }
61
68
  return {
62
69
  jsonrpc: "2.0",
63
70
  id: request.id,
64
- error: {
65
- code: rpcErr.code ?? -32603,
66
- message: rpcErr.message ?? "Internal error",
67
- data: rpcErr.data
68
- }
71
+ error: { code: -32603, message: "Internal error" }
69
72
  };
70
73
  }
71
74
  }
@@ -93,12 +96,22 @@ function createMCPServer(config) {
93
96
  }))
94
97
  };
95
98
  case "tools/call": {
96
- const params = request.params;
97
- const tool = toolMap.get(params.name);
99
+ const params = asParamsObject(request.params, "tools/call");
100
+ const name = params.name;
101
+ if (typeof name !== "string") {
102
+ throw makeError(-32602, 'tools/call requires a string "name"');
103
+ }
104
+ const tool = toolMap.get(name);
98
105
  if (!tool) {
99
- throw makeError(-32602, `Unknown tool: ${params.name}`);
106
+ throw makeError(-32602, `Unknown tool: ${name}`);
100
107
  }
101
- return tool.handler(params.arguments ?? {});
108
+ const args = params.arguments ?? {};
109
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
110
+ throw makeError(-32602, "tool arguments must be an object");
111
+ }
112
+ const argRecord = args;
113
+ validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
114
+ return tool.handler(argRecord);
102
115
  }
103
116
  case "resources/list":
104
117
  return {
@@ -110,12 +123,16 @@ function createMCPServer(config) {
110
123
  }))
111
124
  };
112
125
  case "resources/read": {
113
- const params = request.params;
114
- const resource = findResource(params.uri);
126
+ const params = asParamsObject(request.params, "resources/read");
127
+ const uri = params.uri;
128
+ if (typeof uri !== "string") {
129
+ throw makeError(-32602, 'resources/read requires a string "uri"');
130
+ }
131
+ const resource = findResource(uri);
115
132
  if (!resource) {
116
- throw makeError(-32602, `Unknown resource: ${params.uri}`);
133
+ throw makeError(-32602, `Unknown resource: ${uri}`);
117
134
  }
118
- const content = await resource.handler(params.uri);
135
+ const content = await resource.handler(uri);
119
136
  return { contents: [content] };
120
137
  }
121
138
  case "prompts/list":
@@ -127,12 +144,17 @@ function createMCPServer(config) {
127
144
  }))
128
145
  };
129
146
  case "prompts/get": {
130
- const params = request.params;
131
- const prompt = promptMap.get(params.name);
147
+ const params = asParamsObject(request.params, "prompts/get");
148
+ const name = params.name;
149
+ if (typeof name !== "string") {
150
+ throw makeError(-32602, 'prompts/get requires a string "name"');
151
+ }
152
+ const prompt = promptMap.get(name);
132
153
  if (!prompt) {
133
- throw makeError(-32602, `Unknown prompt: ${params.name}`);
154
+ throw makeError(-32602, `Unknown prompt: ${name}`);
134
155
  }
135
- return prompt.handler(params.arguments ?? {});
156
+ const args = params.arguments ?? {};
157
+ return prompt.handler(args);
136
158
  }
137
159
  case "ping":
138
160
  return {};
@@ -159,10 +181,46 @@ function matchesTemplate(pattern, uri) {
159
181
  function makeError(code, message) {
160
182
  return { code, message };
161
183
  }
184
+ function asParamsObject(params, ctx) {
185
+ if (params === null || typeof params !== "object" || Array.isArray(params)) {
186
+ throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
187
+ }
188
+ return params;
189
+ }
190
+ function validateAgainstSchema(schema, args, toolName) {
191
+ if (!schema || typeof schema !== "object") return;
192
+ const s = schema;
193
+ if (Array.isArray(s.required)) {
194
+ for (const key of s.required) {
195
+ if (typeof key === "string" && !(key in args)) {
196
+ throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
197
+ }
198
+ }
199
+ }
200
+ if (s.properties && typeof s.properties === "object") {
201
+ for (const [key, prop] of Object.entries(s.properties)) {
202
+ const value = args[key];
203
+ if (value === void 0 || value === null) continue;
204
+ const expected = prop?.type;
205
+ if (!expected) continue;
206
+ const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
207
+ if (!ok) {
208
+ throw makeError(
209
+ -32602,
210
+ `Argument "${key}" for tool "${toolName}" must be of type ${expected}`
211
+ );
212
+ }
213
+ }
214
+ }
215
+ }
162
216
 
163
217
  // src/mcp/client.ts
164
218
  function createMCPClient(config) {
165
219
  let requestId = 0;
220
+ const parsedUrl = new URL(config.url);
221
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
222
+ throw new Error(`MCP client URL must be http(s): ${config.url}`);
223
+ }
166
224
  async function rpc(method, params) {
167
225
  const request = {
168
226
  jsonrpc: "2.0",
@@ -177,6 +235,10 @@ function createMCPClient(config) {
177
235
  ...config.headers
178
236
  },
179
237
  body: JSON.stringify(request),
238
+ // never follow redirects: a 3xx to another host would otherwise re-send
239
+ // config.headers (often a bearer token) to that host, leaking the
240
+ // credential and enabling an ssrf pivot.
241
+ redirect: "error",
180
242
  signal: AbortSignal.timeout(3e4)
181
243
  });
182
244
  if (!response.ok) {
@@ -250,6 +312,8 @@ function createMCPClient(config) {
250
312
  // src/mcp/handler.ts
251
313
  function createMCPHandler(server, options) {
252
314
  const corsOrigin = options?.corsOrigin;
315
+ const allowedOrigins = options?.allowedOrigins;
316
+ const authorize = options?.authorize;
253
317
  return async (req, res) => {
254
318
  if (corsOrigin) {
255
319
  res.setHeader("Access-Control-Allow-Origin", corsOrigin);
@@ -261,6 +325,31 @@ function createMCPHandler(server, options) {
261
325
  res.end();
262
326
  return;
263
327
  }
328
+ const origin = req.headers.origin;
329
+ if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
330
+ res.writeHead(403, { "Content-Type": "text/plain" });
331
+ res.end("Forbidden");
332
+ return;
333
+ }
334
+ if (authorize) {
335
+ let allowed = false;
336
+ try {
337
+ allowed = await authorize(req);
338
+ } catch {
339
+ allowed = false;
340
+ }
341
+ if (!allowed) {
342
+ res.writeHead(401, { "Content-Type": "application/json" });
343
+ res.end(
344
+ JSON.stringify({
345
+ jsonrpc: "2.0",
346
+ id: null,
347
+ error: { code: -32600, message: "Unauthorized" }
348
+ })
349
+ );
350
+ return;
351
+ }
352
+ }
264
353
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
265
354
  if (url.pathname.endsWith("/sse") && req.method === "GET") {
266
355
  handleSSE(server, req, res);
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema } from '../types-FSnS43LM.cjs';
2
- import { T as ToolHandler } from '../ai-B2NkPypc.cjs';
1
+ import { J as JsonSchema } from '../types-BcCKlL06.cjs';
2
+ import { T as ToolHandler } from '../ai-dyp9MfTz.cjs';
3
3
  import { IncomingMessage, ServerResponse } from 'node:http';
4
4
 
5
5
  interface JsonRpcRequest {
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
174
174
 
175
175
  interface MCPHandlerOptions {
176
176
  corsOrigin?: string;
177
+ /**
178
+ * allow-list of permitted `Origin` header values. when set, any request
179
+ * carrying an `Origin` not in the list is rejected with 403. this is the
180
+ * primary defence against dns-rebinding attacks on a locally-bound server.
181
+ */
182
+ allowedOrigins?: string[];
183
+ /**
184
+ * authorisation gate run before any request is dispatched to the server.
185
+ * return false (or throw) to reject with 401. an mcp server exposes tool
186
+ * execution, so it must not be reachable on a network without an authz
187
+ * check — there is intentionally no default-allow for remote callers.
188
+ */
189
+ authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
177
190
  }
178
191
  /**
179
192
  * Create a Node.js HTTP handler for an MCP server.
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema } from '../types-FSnS43LM.js';
2
- import { T as ToolHandler } from '../ai-VMRaOOak.js';
1
+ import { J as JsonSchema } from '../types-BcCKlL06.js';
2
+ import { T as ToolHandler } from '../ai-B65XFWfZ.js';
3
3
  import { IncomingMessage, ServerResponse } from 'node:http';
4
4
 
5
5
  interface JsonRpcRequest {
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
174
174
 
175
175
  interface MCPHandlerOptions {
176
176
  corsOrigin?: string;
177
+ /**
178
+ * allow-list of permitted `Origin` header values. when set, any request
179
+ * carrying an `Origin` not in the list is rejected with 403. this is the
180
+ * primary defence against dns-rebinding attacks on a locally-bound server.
181
+ */
182
+ allowedOrigins?: string[];
183
+ /**
184
+ * authorisation gate run before any request is dispatched to the server.
185
+ * return false (or throw) to reject with 401. an mcp server exposes tool
186
+ * execution, so it must not be reachable on a network without an authz
187
+ * check — there is intentionally no default-allow for remote callers.
188
+ */
189
+ authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
177
190
  }
178
191
  /**
179
192
  * Create a Node.js HTTP handler for an MCP server.
package/dist/mcp/index.js CHANGED
@@ -30,14 +30,17 @@ function createMCPServer(config) {
30
30
  return { jsonrpc: "2.0", id: request.id, result };
31
31
  } catch (err) {
32
32
  const rpcErr = err;
33
+ if (typeof rpcErr.code === "number") {
34
+ return {
35
+ jsonrpc: "2.0",
36
+ id: request.id,
37
+ error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
38
+ };
39
+ }
33
40
  return {
34
41
  jsonrpc: "2.0",
35
42
  id: request.id,
36
- error: {
37
- code: rpcErr.code ?? -32603,
38
- message: rpcErr.message ?? "Internal error",
39
- data: rpcErr.data
40
- }
43
+ error: { code: -32603, message: "Internal error" }
41
44
  };
42
45
  }
43
46
  }
@@ -65,12 +68,22 @@ function createMCPServer(config) {
65
68
  }))
66
69
  };
67
70
  case "tools/call": {
68
- const params = request.params;
69
- const tool = toolMap.get(params.name);
71
+ const params = asParamsObject(request.params, "tools/call");
72
+ const name = params.name;
73
+ if (typeof name !== "string") {
74
+ throw makeError(-32602, 'tools/call requires a string "name"');
75
+ }
76
+ const tool = toolMap.get(name);
70
77
  if (!tool) {
71
- throw makeError(-32602, `Unknown tool: ${params.name}`);
78
+ throw makeError(-32602, `Unknown tool: ${name}`);
72
79
  }
73
- return tool.handler(params.arguments ?? {});
80
+ const args = params.arguments ?? {};
81
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
82
+ throw makeError(-32602, "tool arguments must be an object");
83
+ }
84
+ const argRecord = args;
85
+ validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
86
+ return tool.handler(argRecord);
74
87
  }
75
88
  case "resources/list":
76
89
  return {
@@ -82,12 +95,16 @@ function createMCPServer(config) {
82
95
  }))
83
96
  };
84
97
  case "resources/read": {
85
- const params = request.params;
86
- const resource = findResource(params.uri);
98
+ const params = asParamsObject(request.params, "resources/read");
99
+ const uri = params.uri;
100
+ if (typeof uri !== "string") {
101
+ throw makeError(-32602, 'resources/read requires a string "uri"');
102
+ }
103
+ const resource = findResource(uri);
87
104
  if (!resource) {
88
- throw makeError(-32602, `Unknown resource: ${params.uri}`);
105
+ throw makeError(-32602, `Unknown resource: ${uri}`);
89
106
  }
90
- const content = await resource.handler(params.uri);
107
+ const content = await resource.handler(uri);
91
108
  return { contents: [content] };
92
109
  }
93
110
  case "prompts/list":
@@ -99,12 +116,17 @@ function createMCPServer(config) {
99
116
  }))
100
117
  };
101
118
  case "prompts/get": {
102
- const params = request.params;
103
- const prompt = promptMap.get(params.name);
119
+ const params = asParamsObject(request.params, "prompts/get");
120
+ const name = params.name;
121
+ if (typeof name !== "string") {
122
+ throw makeError(-32602, 'prompts/get requires a string "name"');
123
+ }
124
+ const prompt = promptMap.get(name);
104
125
  if (!prompt) {
105
- throw makeError(-32602, `Unknown prompt: ${params.name}`);
126
+ throw makeError(-32602, `Unknown prompt: ${name}`);
106
127
  }
107
- return prompt.handler(params.arguments ?? {});
128
+ const args = params.arguments ?? {};
129
+ return prompt.handler(args);
108
130
  }
109
131
  case "ping":
110
132
  return {};
@@ -131,10 +153,46 @@ function matchesTemplate(pattern, uri) {
131
153
  function makeError(code, message) {
132
154
  return { code, message };
133
155
  }
156
+ function asParamsObject(params, ctx) {
157
+ if (params === null || typeof params !== "object" || Array.isArray(params)) {
158
+ throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
159
+ }
160
+ return params;
161
+ }
162
+ function validateAgainstSchema(schema, args, toolName) {
163
+ if (!schema || typeof schema !== "object") return;
164
+ const s = schema;
165
+ if (Array.isArray(s.required)) {
166
+ for (const key of s.required) {
167
+ if (typeof key === "string" && !(key in args)) {
168
+ throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
169
+ }
170
+ }
171
+ }
172
+ if (s.properties && typeof s.properties === "object") {
173
+ for (const [key, prop] of Object.entries(s.properties)) {
174
+ const value = args[key];
175
+ if (value === void 0 || value === null) continue;
176
+ const expected = prop?.type;
177
+ if (!expected) continue;
178
+ const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
179
+ if (!ok) {
180
+ throw makeError(
181
+ -32602,
182
+ `Argument "${key}" for tool "${toolName}" must be of type ${expected}`
183
+ );
184
+ }
185
+ }
186
+ }
187
+ }
134
188
 
135
189
  // src/mcp/client.ts
136
190
  function createMCPClient(config) {
137
191
  let requestId = 0;
192
+ const parsedUrl = new URL(config.url);
193
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
194
+ throw new Error(`MCP client URL must be http(s): ${config.url}`);
195
+ }
138
196
  async function rpc(method, params) {
139
197
  const request = {
140
198
  jsonrpc: "2.0",
@@ -149,6 +207,10 @@ function createMCPClient(config) {
149
207
  ...config.headers
150
208
  },
151
209
  body: JSON.stringify(request),
210
+ // never follow redirects: a 3xx to another host would otherwise re-send
211
+ // config.headers (often a bearer token) to that host, leaking the
212
+ // credential and enabling an ssrf pivot.
213
+ redirect: "error",
152
214
  signal: AbortSignal.timeout(3e4)
153
215
  });
154
216
  if (!response.ok) {
@@ -222,6 +284,8 @@ function createMCPClient(config) {
222
284
  // src/mcp/handler.ts
223
285
  function createMCPHandler(server, options) {
224
286
  const corsOrigin = options?.corsOrigin;
287
+ const allowedOrigins = options?.allowedOrigins;
288
+ const authorize = options?.authorize;
225
289
  return async (req, res) => {
226
290
  if (corsOrigin) {
227
291
  res.setHeader("Access-Control-Allow-Origin", corsOrigin);
@@ -233,6 +297,31 @@ function createMCPHandler(server, options) {
233
297
  res.end();
234
298
  return;
235
299
  }
300
+ const origin = req.headers.origin;
301
+ if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
302
+ res.writeHead(403, { "Content-Type": "text/plain" });
303
+ res.end("Forbidden");
304
+ return;
305
+ }
306
+ if (authorize) {
307
+ let allowed = false;
308
+ try {
309
+ allowed = await authorize(req);
310
+ } catch {
311
+ allowed = false;
312
+ }
313
+ if (!allowed) {
314
+ res.writeHead(401, { "Content-Type": "application/json" });
315
+ res.end(
316
+ JSON.stringify({
317
+ jsonrpc: "2.0",
318
+ id: null,
319
+ error: { code: -32600, message: "Unauthorized" }
320
+ })
321
+ );
322
+ return;
323
+ }
324
+ }
236
325
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
237
326
  if (url.pathname.endsWith("/sse") && req.method === "GET") {
238
327
  handleSSE(server, req, res);
@@ -0,0 +1,157 @@
1
+ type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ interface TextContent {
3
+ type: 'text';
4
+ text: string;
5
+ }
6
+ interface ImageContent {
7
+ type: 'image';
8
+ /** Base64-encoded image data or a URL. */
9
+ source: string;
10
+ mediaType?: string;
11
+ }
12
+ interface ToolCallContent {
13
+ type: 'tool_call';
14
+ id: string;
15
+ name: string;
16
+ arguments: Record<string, unknown>;
17
+ }
18
+ interface ToolResultContent {
19
+ type: 'tool_result';
20
+ id: string;
21
+ content: string;
22
+ isError?: boolean;
23
+ }
24
+ type MessageContent = string | TextContent | ImageContent | ToolCallContent | ToolResultContent;
25
+ interface ChatMessage {
26
+ role: MessageRole;
27
+ content: MessageContent | MessageContent[];
28
+ name?: string;
29
+ }
30
+ interface ToolDefinition {
31
+ name: string;
32
+ description: string;
33
+ parameters: JsonSchema;
34
+ }
35
+ interface JsonSchema {
36
+ type: string;
37
+ properties?: Record<string, JsonSchema & {
38
+ description?: string;
39
+ }>;
40
+ required?: string[];
41
+ items?: JsonSchema;
42
+ enum?: unknown[];
43
+ description?: string;
44
+ [key: string]: unknown;
45
+ }
46
+ interface ChatRequest {
47
+ messages: ChatMessage[];
48
+ model?: string;
49
+ temperature?: number;
50
+ maxTokens?: number;
51
+ topP?: number;
52
+ stop?: string[];
53
+ tools?: ToolDefinition[];
54
+ toolChoice?: 'auto' | 'none' | 'required' | {
55
+ name: string;
56
+ };
57
+ /** Adapter-specific options passed through untouched. */
58
+ extra?: Record<string, unknown>;
59
+ }
60
+ interface ToolCall {
61
+ id: string;
62
+ name: string;
63
+ arguments: Record<string, unknown>;
64
+ }
65
+ interface ChatResponse {
66
+ content: string;
67
+ toolCalls?: ToolCall[];
68
+ finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
69
+ usage?: TokenUsage;
70
+ /** Raw response from the provider for advanced use cases. */
71
+ raw?: unknown;
72
+ }
73
+ interface TokenUsage {
74
+ promptTokens: number;
75
+ completionTokens: number;
76
+ totalTokens: number;
77
+ }
78
+ interface ChatChunk {
79
+ /** Incremental text delta. */
80
+ delta: string;
81
+ /** Incremental tool call delta (partial). */
82
+ toolCallDelta?: Partial<ToolCall> & {
83
+ index?: number;
84
+ };
85
+ /** Set on the final chunk. */
86
+ finishReason?: ChatResponse['finishReason'];
87
+ /** Set on the final chunk. */
88
+ usage?: TokenUsage;
89
+ }
90
+ interface EmbeddingRequest {
91
+ input: string | string[];
92
+ model?: string;
93
+ /** Adapter-specific options. */
94
+ extra?: Record<string, unknown>;
95
+ }
96
+ interface EmbeddingResponse {
97
+ embeddings: number[][];
98
+ usage?: {
99
+ totalTokens: number;
100
+ };
101
+ raw?: unknown;
102
+ }
103
+ interface AIAdapter {
104
+ /** Send a chat completion request. */
105
+ chat(request: ChatRequest): Promise<ChatResponse>;
106
+ /** Stream a chat completion. Adapters may omit this (falls back to chat). */
107
+ stream?(request: ChatRequest): AsyncIterable<ChatChunk>;
108
+ /** Generate embeddings. Optional capability. */
109
+ embeddings?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
110
+ }
111
+ interface OpenAIConfig {
112
+ apiKey: string;
113
+ baseURL?: string;
114
+ organization?: string;
115
+ defaultModel?: string;
116
+ }
117
+ interface AnthropicConfig {
118
+ apiKey: string;
119
+ baseURL?: string;
120
+ defaultModel?: string;
121
+ }
122
+ interface GoogleConfig {
123
+ apiKey: string;
124
+ defaultModel?: string;
125
+ }
126
+ interface OllamaConfig {
127
+ baseURL?: string;
128
+ defaultModel?: string;
129
+ }
130
+ interface AIHooks {
131
+ /** Called before every chat request. Can modify the request. */
132
+ onBeforeChat?: (request: ChatRequest) => ChatRequest | Promise<ChatRequest>;
133
+ /** Called after every chat response. Can modify the response. */
134
+ onAfterChat?: (response: ChatResponse, request: ChatRequest) => ChatResponse | Promise<ChatResponse>;
135
+ /**
136
+ * Called on any adapter error.
137
+ *
138
+ * `context.request` carries the full prompt (and any provider error may
139
+ * originate from the underlying SDK and reference request metadata), so do
140
+ * not forward these verbatim to an untrusted or shared log sink — redact or
141
+ * summarise first.
142
+ */
143
+ onError?: (error: Error, context: {
144
+ method: string;
145
+ request?: ChatRequest;
146
+ }) => void;
147
+ }
148
+ interface RetryConfig {
149
+ /** Max number of retries (default: 0 = no retry). */
150
+ maxRetries?: number;
151
+ /** Base delay in ms (default: 1000). Doubles on each attempt (exponential backoff). */
152
+ baseDelay?: number;
153
+ /** Whether to retry on this error. Default: retries on network errors and 429/500+ status codes. */
154
+ shouldRetry?: (error: Error) => boolean;
155
+ }
156
+
157
+ export type { AIAdapter as A, ChatChunk as C, EmbeddingRequest as E, GoogleConfig as G, ImageContent as I, JsonSchema as J, MessageContent as M, OllamaConfig as O, RetryConfig as R, TextContent as T, AIHooks as a, AnthropicConfig as b, ChatMessage as c, ChatRequest as d, ChatResponse as e, EmbeddingResponse as f, MessageRole as g, OpenAIConfig as h, TokenUsage as i, ToolCall as j, ToolCallContent as k, ToolDefinition as l, ToolResultContent as m };
@@ -0,0 +1,157 @@
1
+ type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ interface TextContent {
3
+ type: 'text';
4
+ text: string;
5
+ }
6
+ interface ImageContent {
7
+ type: 'image';
8
+ /** Base64-encoded image data or a URL. */
9
+ source: string;
10
+ mediaType?: string;
11
+ }
12
+ interface ToolCallContent {
13
+ type: 'tool_call';
14
+ id: string;
15
+ name: string;
16
+ arguments: Record<string, unknown>;
17
+ }
18
+ interface ToolResultContent {
19
+ type: 'tool_result';
20
+ id: string;
21
+ content: string;
22
+ isError?: boolean;
23
+ }
24
+ type MessageContent = string | TextContent | ImageContent | ToolCallContent | ToolResultContent;
25
+ interface ChatMessage {
26
+ role: MessageRole;
27
+ content: MessageContent | MessageContent[];
28
+ name?: string;
29
+ }
30
+ interface ToolDefinition {
31
+ name: string;
32
+ description: string;
33
+ parameters: JsonSchema;
34
+ }
35
+ interface JsonSchema {
36
+ type: string;
37
+ properties?: Record<string, JsonSchema & {
38
+ description?: string;
39
+ }>;
40
+ required?: string[];
41
+ items?: JsonSchema;
42
+ enum?: unknown[];
43
+ description?: string;
44
+ [key: string]: unknown;
45
+ }
46
+ interface ChatRequest {
47
+ messages: ChatMessage[];
48
+ model?: string;
49
+ temperature?: number;
50
+ maxTokens?: number;
51
+ topP?: number;
52
+ stop?: string[];
53
+ tools?: ToolDefinition[];
54
+ toolChoice?: 'auto' | 'none' | 'required' | {
55
+ name: string;
56
+ };
57
+ /** Adapter-specific options passed through untouched. */
58
+ extra?: Record<string, unknown>;
59
+ }
60
+ interface ToolCall {
61
+ id: string;
62
+ name: string;
63
+ arguments: Record<string, unknown>;
64
+ }
65
+ interface ChatResponse {
66
+ content: string;
67
+ toolCalls?: ToolCall[];
68
+ finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
69
+ usage?: TokenUsage;
70
+ /** Raw response from the provider for advanced use cases. */
71
+ raw?: unknown;
72
+ }
73
+ interface TokenUsage {
74
+ promptTokens: number;
75
+ completionTokens: number;
76
+ totalTokens: number;
77
+ }
78
+ interface ChatChunk {
79
+ /** Incremental text delta. */
80
+ delta: string;
81
+ /** Incremental tool call delta (partial). */
82
+ toolCallDelta?: Partial<ToolCall> & {
83
+ index?: number;
84
+ };
85
+ /** Set on the final chunk. */
86
+ finishReason?: ChatResponse['finishReason'];
87
+ /** Set on the final chunk. */
88
+ usage?: TokenUsage;
89
+ }
90
+ interface EmbeddingRequest {
91
+ input: string | string[];
92
+ model?: string;
93
+ /** Adapter-specific options. */
94
+ extra?: Record<string, unknown>;
95
+ }
96
+ interface EmbeddingResponse {
97
+ embeddings: number[][];
98
+ usage?: {
99
+ totalTokens: number;
100
+ };
101
+ raw?: unknown;
102
+ }
103
+ interface AIAdapter {
104
+ /** Send a chat completion request. */
105
+ chat(request: ChatRequest): Promise<ChatResponse>;
106
+ /** Stream a chat completion. Adapters may omit this (falls back to chat). */
107
+ stream?(request: ChatRequest): AsyncIterable<ChatChunk>;
108
+ /** Generate embeddings. Optional capability. */
109
+ embeddings?(request: EmbeddingRequest): Promise<EmbeddingResponse>;
110
+ }
111
+ interface OpenAIConfig {
112
+ apiKey: string;
113
+ baseURL?: string;
114
+ organization?: string;
115
+ defaultModel?: string;
116
+ }
117
+ interface AnthropicConfig {
118
+ apiKey: string;
119
+ baseURL?: string;
120
+ defaultModel?: string;
121
+ }
122
+ interface GoogleConfig {
123
+ apiKey: string;
124
+ defaultModel?: string;
125
+ }
126
+ interface OllamaConfig {
127
+ baseURL?: string;
128
+ defaultModel?: string;
129
+ }
130
+ interface AIHooks {
131
+ /** Called before every chat request. Can modify the request. */
132
+ onBeforeChat?: (request: ChatRequest) => ChatRequest | Promise<ChatRequest>;
133
+ /** Called after every chat response. Can modify the response. */
134
+ onAfterChat?: (response: ChatResponse, request: ChatRequest) => ChatResponse | Promise<ChatResponse>;
135
+ /**
136
+ * Called on any adapter error.
137
+ *
138
+ * `context.request` carries the full prompt (and any provider error may
139
+ * originate from the underlying SDK and reference request metadata), so do
140
+ * not forward these verbatim to an untrusted or shared log sink — redact or
141
+ * summarise first.
142
+ */
143
+ onError?: (error: Error, context: {
144
+ method: string;
145
+ request?: ChatRequest;
146
+ }) => void;
147
+ }
148
+ interface RetryConfig {
149
+ /** Max number of retries (default: 0 = no retry). */
150
+ maxRetries?: number;
151
+ /** Base delay in ms (default: 1000). Doubles on each attempt (exponential backoff). */
152
+ baseDelay?: number;
153
+ /** Whether to retry on this error. Default: retries on network errors and 429/500+ status codes. */
154
+ shouldRetry?: (error: Error) => boolean;
155
+ }
156
+
157
+ export type { AIAdapter as A, ChatChunk as C, EmbeddingRequest as E, GoogleConfig as G, ImageContent as I, JsonSchema as J, MessageContent as M, OllamaConfig as O, RetryConfig as R, TextContent as T, AIHooks as a, AnthropicConfig as b, ChatMessage as c, ChatRequest as d, ChatResponse as e, EmbeddingResponse as f, MessageRole as g, OpenAIConfig as h, TokenUsage as i, ToolCall as j, ToolCallContent as k, ToolDefinition as l, ToolResultContent as m };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-ai",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "AI adapters and MCP support for UtopiaJS",
5
5
  "type": "module",
6
6
  "license": "MIT",