@dudko.dev/agent-web 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.cjs CHANGED
@@ -30,6 +30,7 @@ var import_client = require("@modelcontextprotocol/sdk/client/index.js");
30
30
  var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
31
31
  var import_ai = require("ai");
32
32
  var MAX_TOOL_NAME_LEN = 64;
33
+ var sanitizeName = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
33
34
  var flattenContent = (content) => {
34
35
  if (!Array.isArray(content)) return content;
35
36
  const parts = content;
@@ -37,41 +38,36 @@ var flattenContent = (content) => {
37
38
  if (allText) return parts.map((p) => p.text).join("\n");
38
39
  return parts.map((p) => p?.type === "text" && typeof p.text === "string" ? p.text : p);
39
40
  };
40
- var connectMcpHttp = async (servers, opts = {}) => {
41
- const log = opts.onLog ?? (() => {
42
- });
43
- const clients = /* @__PURE__ */ new Map();
44
- const tools = {};
45
- const catalog = [];
46
- const results = [];
47
- for (const [name, cfg] of Object.entries(servers)) {
48
- try {
49
- if (cfg.headers && cfg.getHeaders) {
50
- throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
41
+ var connectOne = async (name, cfg, opts, log) => {
42
+ try {
43
+ if (cfg.headers && cfg.getHeaders) {
44
+ throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
45
+ }
46
+ const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
47
+ const transport = new import_streamableHttp.StreamableHTTPClientTransport(new URL(cfg.url), {
48
+ requestInit: headers ? { headers } : void 0
49
+ });
50
+ const client = new import_client.Client({
51
+ name: opts.clientName ?? "agent-web",
52
+ version: opts.clientVersion ?? "0.0.0"
53
+ });
54
+ await client.connect(transport);
55
+ const listed = await client.listTools();
56
+ const mounts = [];
57
+ for (const t of listed.tools) {
58
+ const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`;
59
+ if (prefixed.length > MAX_TOOL_NAME_LEN) {
60
+ log(
61
+ "warn",
62
+ `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
63
+ );
64
+ continue;
51
65
  }
52
- const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
53
- const transport = new import_streamableHttp.StreamableHTTPClientTransport(new URL(cfg.url), {
54
- requestInit: headers ? { headers } : void 0
55
- });
56
- const client = new import_client.Client({
57
- name: opts.clientName ?? "agent-web",
58
- version: opts.clientVersion ?? "0.0.0"
59
- });
60
- await client.connect(transport);
61
- clients.set(name, client);
62
- const listed = await client.listTools();
63
- let mounted = 0;
64
- for (const t of listed.tools) {
65
- const prefixed = `${name}__${t.name}`;
66
- if (prefixed.length > MAX_TOOL_NAME_LEN) {
67
- log(
68
- "warn",
69
- `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
70
- );
71
- continue;
72
- }
73
- const description = t.description ?? "";
74
- tools[prefixed] = (0, import_ai.dynamicTool)({
66
+ const description = t.description ?? "";
67
+ mounts.push({
68
+ key: prefixed,
69
+ description,
70
+ tool: (0, import_ai.dynamicTool)({
75
71
  description,
76
72
  inputSchema: (0, import_ai.jsonSchema)(t.inputSchema),
77
73
  execute: async (args, options) => {
@@ -80,19 +76,50 @@ var connectMcpHttp = async (servers, opts = {}) => {
80
76
  void 0,
81
77
  options?.abortSignal ? { signal: options.abortSignal } : void 0
82
78
  );
83
- return flattenContent(res.content);
79
+ const flat = flattenContent(res.content);
80
+ if (res.isError) {
81
+ throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
82
+ }
83
+ return flat;
84
84
  }
85
- });
86
- catalog.push({ name: prefixed, description, server: name });
87
- mounted += 1;
85
+ })
86
+ });
87
+ }
88
+ return { name, client, mounts };
89
+ } catch (err) {
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ log("error", `[mcp] ${name}: failed to connect - ${message}`);
92
+ return { name, mounts: [], error: message };
93
+ }
94
+ };
95
+ var connectMcpHttp = async (servers, opts = {}) => {
96
+ const log = opts.onLog ?? (() => {
97
+ });
98
+ const clients = /* @__PURE__ */ new Map();
99
+ const tools = {};
100
+ const catalog = [];
101
+ const results = [];
102
+ const settled = await Promise.all(
103
+ Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log))
104
+ );
105
+ for (const server of settled) {
106
+ if (server.error !== void 0) {
107
+ results.push({ name: server.name, connected: false, error: server.error });
108
+ continue;
109
+ }
110
+ if (server.client) clients.set(server.name, server.client);
111
+ let mounted = 0;
112
+ for (const m of server.mounts) {
113
+ if (tools[m.key]) {
114
+ log("warn", `[mcp] ${server.name}: duplicate tool name "${m.key}"; keeping the first`);
115
+ continue;
88
116
  }
89
- log("info", `[mcp] ${name}: ${mounted} tools mounted`);
90
- results.push({ name, connected: true });
91
- } catch (err) {
92
- const message = err instanceof Error ? err.message : String(err);
93
- log("error", `[mcp] ${name}: failed to connect - ${message}`);
94
- results.push({ name, connected: false, error: message });
117
+ tools[m.key] = m.tool;
118
+ catalog.push({ name: m.key, description: m.description, server: server.name });
119
+ mounted += 1;
95
120
  }
121
+ log("info", `[mcp] ${server.name}: ${mounted} tools mounted`);
122
+ results.push({ name: server.name, connected: true });
96
123
  }
97
124
  return {
98
125
  tools,
package/dist/mcp.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp/index.ts","../src/mcp/http.ts"],"sourcesContent":["export { connectMcpHttp, flattenContent } from './http.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\n","import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: { name: string; connected: boolean; error?: string }[]\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\n/** Connect to one or more HTTP MCP servers and return their tools + catalogue. */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: ConnectedMcp['results'] = []\n\n for (const [name, cfg] of Object.entries(servers)) {\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n })\n const client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n clients.set(name, client)\n\n const listed = await client.listTools()\n let mounted = 0\n for (const t of listed.tools) {\n const prefixed = `${name}__${t.name}`\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const description = t.description ?? ''\n tools[prefixed] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n return flattenContent(res.content)\n },\n })\n catalog.push({ name: prefixed, description, server: name })\n mounted += 1\n }\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n results.push({ name, connected: true })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log('error', `[mcp] ${name}: failed to connect - ${message}`)\n results.push({ name, connected: false, error: message })\n }\n }\n\n return {\n tools,\n catalog,\n results,\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/mcp/index.ts","../src/mcp/http.ts"],"sourcesContent":["export { connectMcpHttp, flattenContent } from './http.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\n","import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: { name: string; connected: boolean; error?: string }[]\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface ServerMount {\n name: string\n client?: Client\n mounts: { key: string; description: string; tool: ToolSet[string] }[]\n error?: string\n}\n\nconst connectOne = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerMount> => {\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n })\n const client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n const listed = await client.listTools()\n const mounts: ServerMount['mounts'] = []\n for (const t of listed.tools) {\n const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const description = t.description ?? ''\n mounts.push({\n key: prefixed,\n description,\n tool: dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n }),\n })\n }\n return { name, client, mounts }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log('error', `[mcp] ${name}: failed to connect - ${message}`)\n return { name, mounts: [], error: message }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: ConnectedMcp['results'] = []\n\n const settled = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log)),\n )\n\n for (const server of settled) {\n if (server.error !== undefined) {\n results.push({ name: server.name, connected: false, error: server.error })\n continue\n }\n if (server.client) clients.set(server.name, server.client)\n let mounted = 0\n for (const m of server.mounts) {\n if (tools[m.key]) {\n log('warn', `[mcp] ${server.name}: duplicate tool name \"${m.key}\"; keeping the first`)\n continue\n }\n tools[m.key] = m.tool\n catalog.push({ name: m.key, description: m.description, server: server.name })\n mounted += 1\n }\n log('info', `[mcp] ${server.name}: ${mounted} tools mounted`)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n"],"mappings":"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","names":[]}
package/dist/mcp.d.cts CHANGED
@@ -41,7 +41,11 @@ interface ConnectMcpOptions {
41
41
  * array of text + raw parts for the host to handle.
42
42
  */
43
43
  declare const flattenContent: (content: unknown) => unknown;
44
- /** Connect to one or more HTTP MCP servers and return their tools + catalogue. */
44
+ /**
45
+ * Connect to one or more HTTP MCP servers and return their tools + catalogue.
46
+ * Servers connect concurrently; tools, catalogue, and results merge in the
47
+ * caller's declaration order, so the outcome is deterministic.
48
+ */
45
49
  declare const connectMcpHttp: (servers: Record<string, McpHttpServerConfig>, opts?: ConnectMcpOptions) => Promise<ConnectedMcp>;
46
50
 
47
51
  export { type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, connectMcpHttp, flattenContent };
package/dist/mcp.d.ts CHANGED
@@ -41,7 +41,11 @@ interface ConnectMcpOptions {
41
41
  * array of text + raw parts for the host to handle.
42
42
  */
43
43
  declare const flattenContent: (content: unknown) => unknown;
44
- /** Connect to one or more HTTP MCP servers and return their tools + catalogue. */
44
+ /**
45
+ * Connect to one or more HTTP MCP servers and return their tools + catalogue.
46
+ * Servers connect concurrently; tools, catalogue, and results merge in the
47
+ * caller's declaration order, so the outcome is deterministic.
48
+ */
45
49
  declare const connectMcpHttp: (servers: Record<string, McpHttpServerConfig>, opts?: ConnectMcpOptions) => Promise<ConnectedMcp>;
46
50
 
47
51
  export { type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, connectMcpHttp, flattenContent };
package/dist/mcp.js CHANGED
@@ -3,6 +3,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
4
  import { dynamicTool, jsonSchema } from "ai";
5
5
  var MAX_TOOL_NAME_LEN = 64;
6
+ var sanitizeName = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
6
7
  var flattenContent = (content) => {
7
8
  if (!Array.isArray(content)) return content;
8
9
  const parts = content;
@@ -10,41 +11,36 @@ var flattenContent = (content) => {
10
11
  if (allText) return parts.map((p) => p.text).join("\n");
11
12
  return parts.map((p) => p?.type === "text" && typeof p.text === "string" ? p.text : p);
12
13
  };
13
- var connectMcpHttp = async (servers, opts = {}) => {
14
- const log = opts.onLog ?? (() => {
15
- });
16
- const clients = /* @__PURE__ */ new Map();
17
- const tools = {};
18
- const catalog = [];
19
- const results = [];
20
- for (const [name, cfg] of Object.entries(servers)) {
21
- try {
22
- if (cfg.headers && cfg.getHeaders) {
23
- throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
14
+ var connectOne = async (name, cfg, opts, log) => {
15
+ try {
16
+ if (cfg.headers && cfg.getHeaders) {
17
+ throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
18
+ }
19
+ const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
20
+ const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
21
+ requestInit: headers ? { headers } : void 0
22
+ });
23
+ const client = new Client({
24
+ name: opts.clientName ?? "agent-web",
25
+ version: opts.clientVersion ?? "0.0.0"
26
+ });
27
+ await client.connect(transport);
28
+ const listed = await client.listTools();
29
+ const mounts = [];
30
+ for (const t of listed.tools) {
31
+ const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`;
32
+ if (prefixed.length > MAX_TOOL_NAME_LEN) {
33
+ log(
34
+ "warn",
35
+ `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
36
+ );
37
+ continue;
24
38
  }
25
- const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
26
- const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
27
- requestInit: headers ? { headers } : void 0
28
- });
29
- const client = new Client({
30
- name: opts.clientName ?? "agent-web",
31
- version: opts.clientVersion ?? "0.0.0"
32
- });
33
- await client.connect(transport);
34
- clients.set(name, client);
35
- const listed = await client.listTools();
36
- let mounted = 0;
37
- for (const t of listed.tools) {
38
- const prefixed = `${name}__${t.name}`;
39
- if (prefixed.length > MAX_TOOL_NAME_LEN) {
40
- log(
41
- "warn",
42
- `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
43
- );
44
- continue;
45
- }
46
- const description = t.description ?? "";
47
- tools[prefixed] = dynamicTool({
39
+ const description = t.description ?? "";
40
+ mounts.push({
41
+ key: prefixed,
42
+ description,
43
+ tool: dynamicTool({
48
44
  description,
49
45
  inputSchema: jsonSchema(t.inputSchema),
50
46
  execute: async (args, options) => {
@@ -53,19 +49,50 @@ var connectMcpHttp = async (servers, opts = {}) => {
53
49
  void 0,
54
50
  options?.abortSignal ? { signal: options.abortSignal } : void 0
55
51
  );
56
- return flattenContent(res.content);
52
+ const flat = flattenContent(res.content);
53
+ if (res.isError) {
54
+ throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
55
+ }
56
+ return flat;
57
57
  }
58
- });
59
- catalog.push({ name: prefixed, description, server: name });
60
- mounted += 1;
58
+ })
59
+ });
60
+ }
61
+ return { name, client, mounts };
62
+ } catch (err) {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ log("error", `[mcp] ${name}: failed to connect - ${message}`);
65
+ return { name, mounts: [], error: message };
66
+ }
67
+ };
68
+ var connectMcpHttp = async (servers, opts = {}) => {
69
+ const log = opts.onLog ?? (() => {
70
+ });
71
+ const clients = /* @__PURE__ */ new Map();
72
+ const tools = {};
73
+ const catalog = [];
74
+ const results = [];
75
+ const settled = await Promise.all(
76
+ Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log))
77
+ );
78
+ for (const server of settled) {
79
+ if (server.error !== void 0) {
80
+ results.push({ name: server.name, connected: false, error: server.error });
81
+ continue;
82
+ }
83
+ if (server.client) clients.set(server.name, server.client);
84
+ let mounted = 0;
85
+ for (const m of server.mounts) {
86
+ if (tools[m.key]) {
87
+ log("warn", `[mcp] ${server.name}: duplicate tool name "${m.key}"; keeping the first`);
88
+ continue;
61
89
  }
62
- log("info", `[mcp] ${name}: ${mounted} tools mounted`);
63
- results.push({ name, connected: true });
64
- } catch (err) {
65
- const message = err instanceof Error ? err.message : String(err);
66
- log("error", `[mcp] ${name}: failed to connect - ${message}`);
67
- results.push({ name, connected: false, error: message });
90
+ tools[m.key] = m.tool;
91
+ catalog.push({ name: m.key, description: m.description, server: server.name });
92
+ mounted += 1;
68
93
  }
94
+ log("info", `[mcp] ${server.name}: ${mounted} tools mounted`);
95
+ results.push({ name: server.name, connected: true });
69
96
  }
70
97
  return {
71
98
  tools,
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp/http.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: { name: string; connected: boolean; error?: string }[]\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\n/** Connect to one or more HTTP MCP servers and return their tools + catalogue. */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: ConnectedMcp['results'] = []\n\n for (const [name, cfg] of Object.entries(servers)) {\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n })\n const client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n clients.set(name, client)\n\n const listed = await client.listTools()\n let mounted = 0\n for (const t of listed.tools) {\n const prefixed = `${name}__${t.name}`\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const description = t.description ?? ''\n tools[prefixed] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n return flattenContent(res.content)\n },\n })\n catalog.push({ name: prefixed, description, server: name })\n mounted += 1\n }\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n results.push({ name, connected: true })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log('error', `[mcp] ${name}: failed to connect - ${message}`)\n results.push({ name, connected: false, error: message })\n }\n }\n\n return {\n tools,\n catalog,\n results,\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/mcp/http.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: { name: string; connected: boolean; error?: string }[]\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface ServerMount {\n name: string\n client?: Client\n mounts: { key: string; description: string; tool: ToolSet[string] }[]\n error?: string\n}\n\nconst connectOne = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerMount> => {\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n })\n const client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n const listed = await client.listTools()\n const mounts: ServerMount['mounts'] = []\n for (const t of listed.tools) {\n const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const description = t.description ?? ''\n mounts.push({\n key: prefixed,\n description,\n tool: dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n }),\n })\n }\n return { name, client, mounts }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log('error', `[mcp] ${name}: failed to connect - ${message}`)\n return { name, mounts: [], error: message }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: ConnectedMcp['results'] = []\n\n const settled = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log)),\n )\n\n for (const server of settled) {\n if (server.error !== undefined) {\n results.push({ name: server.name, connected: false, error: server.error })\n continue\n }\n if (server.client) clients.set(server.name, server.client)\n let mounted = 0\n for (const m of server.mounts) {\n if (tools[m.key]) {\n log('warn', `[mcp] ${server.name}: duplicate tool name \"${m.key}\"; keeping the first`)\n continue\n }\n tools[m.key] = m.tool\n catalog.push({ name: m.key, description: m.description, server: server.name })\n mounted += 1\n }\n log('info', `[mcp] ${server.name}: ${mounted} tools mounted`)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,qCAAqC;AAC9C,SAAS,aAAa,kBAAgC;AAsCtD,IAAM,oBAAoB;AAI1B,IAAM,eAAe,CAAC,MAAsB,EAAE,QAAQ,mBAAmB,GAAG;AAQrE,IAAM,iBAAiB,CAAC,YAA8B;AAC3D,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,QAAM,QAAQ;AACd,QAAM,UACJ,MAAM,SAAS,KAAK,MAAM,MAAM,CAAC,MAAM,GAAG,SAAS,UAAU,OAAO,EAAE,SAAS,QAAQ;AACzF,MAAI,QAAS,QAAO,MAAM,IAAI,CAAC,MAAM,EAAE,IAAc,EAAE,KAAK,IAAI;AAChE,SAAO,MAAM,IAAI,CAAC,MAAO,GAAG,SAAS,UAAU,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,CAAE;AACzF;AASA,IAAM,aAAa,OACjB,MACA,KACA,MACA,QACyB;AACzB,MAAI;AACF,QAAI,IAAI,WAAW,IAAI,YAAY;AACjC,YAAM,IAAI,MAAM,eAAe,IAAI,mDAAmD;AAAA,IACxF;AACA,UAAM,UAAU,IAAI,aAAa,MAAM,IAAI,WAAW,IAAI,IAAI;AAC9D,UAAM,YAAY,IAAI,8BAA8B,IAAI,IAAI,IAAI,GAAG,GAAG;AAAA,MACpE,aAAa,UAAU,EAAE,QAAQ,IAAI;AAAA,IACvC,CAAC;AACD,UAAM,SAAS,IAAI,OAAO;AAAA,MACxB,MAAM,KAAK,cAAc;AAAA,MACzB,SAAS,KAAK,iBAAiB;AAAA,IACjC,CAAC;AACD,UAAM,OAAO,QAAQ,SAAS;AAE9B,UAAM,SAAS,MAAM,OAAO,UAAU;AACtC,UAAM,SAAgC,CAAC;AACvC,eAAW,KAAK,OAAO,OAAO;AAC5B,YAAM,WAAW,GAAG,aAAa,IAAI,CAAC,KAAK,aAAa,EAAE,IAAI,CAAC;AAC/D,UAAI,SAAS,SAAS,mBAAmB;AACvC;AAAA,UACE;AAAA,UACA,SAAS,IAAI,WAAW,EAAE,IAAI,iBAAiB,iBAAiB;AAAA,QAClE;AACA;AAAA,MACF;AACA,YAAM,cAAc,EAAE,eAAe;AACrC,aAAO,KAAK;AAAA,QACV,KAAK;AAAA,QACL;AAAA,QACA,MAAM,YAAY;AAAA,UAChB;AAAA,UACA,aAAa,WAAW,EAAE,WAA+C;AAAA,UACzE,SAAS,OAAO,MAAM,YAAY;AAChC,kBAAM,MAAM,MAAM,OAAO;AAAA,cACvB,EAAE,MAAM,EAAE,MAAM,WAAY,QAAQ,CAAC,EAA8B;AAAA,cACnE;AAAA,cACA,SAAS,cAAc,EAAE,QAAQ,QAAQ,YAAY,IAAI;AAAA,YAC3D;AACA,kBAAM,OAAO,eAAe,IAAI,OAAO;AAIvC,gBAAK,IAA8B,SAAS;AAC1C,oBAAM,IAAI,MAAM,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,IAAI,CAAC;AAAA,YACxE;AACA,mBAAO;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AACA,WAAO,EAAE,MAAM,QAAQ,OAAO;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,QAAI,SAAS,SAAS,IAAI,yBAAyB,OAAO,EAAE;AAC5D,WAAO,EAAE,MAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAAA,EAC5C;AACF;AAOO,IAAM,iBAAiB,OAC5B,SACA,OAA0B,CAAC,MACD;AAC1B,QAAM,MAAM,KAAK,UAAU,MAAM;AAAA,EAAC;AAClC,QAAM,UAAU,oBAAI,IAAoB;AACxC,QAAM,QAAiB,CAAC;AACxB,QAAM,UAA6B,CAAC;AACpC,QAAM,UAAmC,CAAC;AAE1C,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,WAAW,MAAM,KAAK,MAAM,GAAG,CAAC;AAAA,EAC/E;AAEA,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,UAAU,QAAW;AAC9B,cAAQ,KAAK,EAAE,MAAM,OAAO,MAAM,WAAW,OAAO,OAAO,OAAO,MAAM,CAAC;AACzE;AAAA,IACF;AACA,QAAI,OAAO,OAAQ,SAAQ,IAAI,OAAO,MAAM,OAAO,MAAM;AACzD,QAAI,UAAU;AACd,eAAW,KAAK,OAAO,QAAQ;AAC7B,UAAI,MAAM,EAAE,GAAG,GAAG;AAChB,YAAI,QAAQ,SAAS,OAAO,IAAI,0BAA0B,EAAE,GAAG,sBAAsB;AACrF;AAAA,MACF;AACA,YAAM,EAAE,GAAG,IAAI,EAAE;AACjB,cAAQ,KAAK,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,aAAa,QAAQ,OAAO,KAAK,CAAC;AAC7E,iBAAW;AAAA,IACb;AACA,QAAI,QAAQ,SAAS,OAAO,IAAI,KAAK,OAAO,gBAAgB;AAC5D,YAAQ,KAAK,EAAE,MAAM,OAAO,MAAM,WAAW,KAAK,CAAC;AAAA,EACrD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,YAAY;AACjB,YAAM,QAAQ,WAAW,CAAC,GAAG,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAAA,IACtE;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@dudko.dev/agent-web",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Headless, configurable in-browser LLM agent on the Vercel AI SDK: multi-provider bring-your-own-key (OpenAI, Anthropic, Google, xAI, DeepSeek, gateway, openai-compatible) AND local WebGPU/WebLLM, native + prompted tool-calling, structured output, an encrypted IndexedDB token vault, and a plan → execute → replan → synthesize loop. UI-agnostic.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.cjs",
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
+ "unpkg": "./dist/agent-web.js",
11
+ "jsdelivr": "./dist/agent-web.js",
10
12
  "exports": {
11
13
  ".": {
12
14
  "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
@@ -16,6 +18,7 @@
16
18
  "import": { "types": "./dist/mcp.d.ts", "default": "./dist/mcp.js" },
17
19
  "require": { "types": "./dist/mcp.d.cts", "default": "./dist/mcp.cjs" }
18
20
  },
21
+ "./bundle": { "types": "./dist/index.d.ts", "import": "./dist/agent-web.js" },
19
22
  "./package.json": "./package.json"
20
23
  },
21
24
  "files": [
@@ -23,6 +26,9 @@
23
26
  "README.md",
24
27
  "LICENSE"
25
28
  ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
26
32
  "scripts": {
27
33
  "build": "tsup",
28
34
  "typecheck": "tsc --noEmit",