@aexhq/tools-mcp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aexhq/tools-mcp
2
2
 
3
- Connect selected MCP Tools to Brain 0.22 through application `hostEnv`. The application owns the MCP connection, authentication and lifecycle. The bridge never accepts arbitrary model-selected Tool names.
3
+ Connect selected MCP Tools to Brain through application `hostEnv`. The application owns the MCP connection, authentication and lifecycle. The bridge never accepts arbitrary model-selected Tool names.
4
4
 
5
5
  ```js
6
6
  import { hostEnv } from "@aexhq/brain";
@@ -23,11 +23,27 @@ The client SDK owns transport and protocol negotiation. `connectMcp` disables au
23
23
 
24
24
  ## Results and failures
25
25
 
26
- Every returned MCP result is first committed as an `mcp_result` Event with Tool provenance. Successful text, structured content and resource links are presented to the model. PNG, JPEG, GIF and WebP images use the official `aex_tool_output` version 1 convention; Pi/Codex 5.1.0 map them into Brain Tool-result media. Audio, binary embedded resources, asynchronous tasks and interactive continuations are explicitly unsupported.
26
+ MCP images and embedded image/PDF resources require a caller-supplied
27
+ `publishMedia({ bytes, mediaType, index }, context)` callback on `mcpTools`. Return an HTTPS URL
28
+ reachable by the model provider. `context` supplies the session ID, invocation sequence, deadline
29
+ and cancellation signal. The application owns upload authorization and expiry, and can bind this
30
+ callback to Aex's attachment SDK or another store.
27
31
 
28
- An optional `project(output, toolName)` selects a smaller model-visible JSON representation. Its input includes `content`, optional `structuredContent`, and the committed `evidenceSequence`; raw evidence remains in the Event. Image media is carried separately. Brain's existing emitted-Event and model-input limits still apply; exceeding them is an explicit failure.
32
+ Known binary media is published before the normalized `mcp_result` Event is committed, including
33
+ MCP error results. The retained evidence contains URL media and the remaining MCP content;
34
+ image/base64 and resource blobs are removed. A missing or failed publisher produces
35
+ `mcp_media_failed` evidence and a failed Tool result. The completed MCP tool is never repeated to
36
+ repair publication. Audio, other binary resource types, asynchronous tasks and interactive
37
+ continuations fail explicitly.
29
38
 
30
- MCP `isError` becomes a failed Brain Tool result with code `mcp_tool_error`, `retryable: false` and the complete MCP result in `details`. JSON-RPC failures use `mcp_protocol_error` and retain the original numeric code and data in `details`. Known MCP SDK failures use `mcp_sdk_error`; unsupported continuations use `mcp_unsupported_result`. Protocol/transport errors also produce `mcp_failure` evidence while the invocation is active.
39
+ Successful text, structured content and resource links are presented to the model. Published PNG,
40
+ JPEG, GIF, WebP and PDF inputs use `aex_tool_output` version 1. Use the matching URL-media Pi/Codex
41
+ release to map them into Brain Tool-result media. Unsupported binary fields inside arbitrary Tool
42
+ JSON are not recursively transformed.
43
+
44
+ An optional `project(output, toolName)` selects a smaller model-visible JSON representation. Its input includes `content`, optional `structuredContent`, and the committed `evidenceSequence`; normalized evidence remains in the Event. Image and PDF media is carried separately. Brain's existing emitted-Event and model-input limits still apply; exceeding them is an explicit failure.
45
+
46
+ MCP `isError` becomes a failed Brain Tool result with code `mcp_tool_error`, `retryable: false` and the normalized MCP result in `details`. JSON-RPC failures use `mcp_protocol_error` and retain the original numeric code and data in `details`. Known MCP SDK failures use `mcp_sdk_error`; unsupported continuations use `mcp_unsupported_result`. Protocol/transport errors also produce `mcp_failure` evidence while the invocation is active.
31
47
 
32
48
  Invocation deadlines and MCP request timeouts produce `timeout`; explicit SDK cancellation produces `cancelled`. The bridge returns `unknown` only when an invocation may have been dispatched and no reliable terminal result is available, such as connection loss after a mutation. The terminal result has `is_error: true` and code `unknown`; its evidence Event retains the structured transport cause. Timeout and cancellation describe why waiting ended and do not promise rollback. The bridge never automatically repeats a call.
33
49
 
package/dist/index.mjs CHANGED
@@ -1,3 +1,19 @@
1
+ // ../../shared/media.mjs
2
+ function validateMedia(item) {
3
+ if (!item || !(item.type === "image" && Object.keys(item).every((key) => ["type", "url"].includes(key)) || item.type === "file" && item.media_type === "application/pdf" && Object.keys(item).every((key) => ["type", "url", "media_type"].includes(key))) || typeof item.url !== "string") throw new TypeError("media must contain Brain image or PDF URL blocks");
4
+ const url = new URL(item.url);
5
+ if (url.protocol !== "https:" || url.username || url.password) throw new TypeError("media requires HTTPS without credentials");
6
+ return item;
7
+ }
8
+ async function publish(publishMedia, bytes, mediaType, index, context) {
9
+ if (typeof publishMedia !== "function") throw new TypeError("publishMedia is required for model media");
10
+ if (!["image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf"].includes(mediaType)) throw new TypeError(`unsupported media type: ${mediaType}`);
11
+ context.signal.throwIfAborted();
12
+ const url = await publishMedia({ bytes, mediaType, index }, context);
13
+ context.signal.throwIfAborted();
14
+ return validateMedia(mediaType === "application/pdf" ? { type: "file", media_type: mediaType, url } : { type: "image", url });
15
+ }
16
+
1
17
  // src/index.mjs
2
18
  import { Client, ProtocolError, SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
3
19
  import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
@@ -10,7 +26,7 @@ function toolOutput(content, media = []) {
10
26
  }
11
27
 
12
28
  // src/index.mjs
13
- async function connectMcp(transport, { name = "aex-mcp", version = "0.1.0", ...options } = {}) {
29
+ async function connectMcp(transport, { name = "aex-mcp", version = "0.3.0", ...options } = {}) {
14
30
  const client = new Client({ name, version }, { ...options, inputRequired: { autoFulfill: false } });
15
31
  try {
16
32
  await client.connect(transport);
@@ -20,7 +36,7 @@ async function connectMcp(transport, { name = "aex-mcp", version = "0.1.0", ...o
20
36
  }
21
37
  return client;
22
38
  }
23
- async function mcpTools({ client, names, env, prefix = "", project }) {
39
+ async function mcpTools({ client, names, env, prefix = "", project, publishMedia }) {
24
40
  if (inspectEnvironment(env).driver.driver !== "host") throw new TypeError("MCP Tools must be placed in hostEnv");
25
41
  z.array(z.string().min(1)).min(1).parse(names);
26
42
  if (new Set(names).size !== names.length) throw new TypeError("select each MCP Tool once");
@@ -66,23 +82,32 @@ async function mcpTools({ client, names, env, prefix = "", project }) {
66
82
  });
67
83
  return outcome;
68
84
  }
69
- const evidenceSequence = await context.emit("mcp_result", { tool: definition.name, result: response });
85
+ const media = [];
86
+ const content = [];
87
+ try {
88
+ for (const block of response.content ?? []) {
89
+ const encoded = block.type === "image" ? block.data : block.type === "resource" ? block.resource?.blob : void 0;
90
+ if (encoded !== void 0) {
91
+ const bytes = Buffer.from(encoded, "base64");
92
+ if (bytes.toString("base64") !== encoded) throw new TypeError("invalid MCP media base64");
93
+ media.push(await publish(publishMedia, bytes, block.mimeType ?? block.resource?.mimeType, media.length, context));
94
+ } else if (block.type === "text" || block.type === "resource_link" || block.type === "resource" && typeof block.resource?.text === "string") {
95
+ content.push(block);
96
+ } else throw new TypeError(`unsupported MCP content type: ${block.type}`);
97
+ }
98
+ } catch (error) {
99
+ if (context.signal.aborted) throw error;
100
+ await context.emit("mcp_media_failed", { tool: definition.name, published: media });
101
+ return failure("mcp_media_failed", String(error.message), { published: media });
102
+ }
103
+ const retained = { ...response, content, ...media.length ? { media } : {} };
104
+ const evidenceSequence = await context.emit("mcp_result", { tool: definition.name, result: retained });
70
105
  if (response.resultType && response.resultType !== "complete") {
71
- return failure("mcp_unsupported_result", `MCP ${definition.name} requires unsupported continuation (${response.resultType}); evidence ${evidenceSequence}`, response);
106
+ return failure("mcp_unsupported_result", `MCP ${definition.name} requires unsupported continuation (${response.resultType}); evidence ${evidenceSequence}`, retained);
72
107
  }
73
108
  if (response.isError) {
74
- const message = response.content?.filter((block) => block.type === "text").map((block) => block.text).join("\n");
75
- return failure("mcp_tool_error", `MCP ${definition.name}: ${message || "Tool failed"}; evidence ${evidenceSequence}`, response);
76
- }
77
- const media = [];
78
- const content = [];
79
- for (const block of response.content ?? []) {
80
- if (block.type === "image") {
81
- if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(block.mimeType)) throw new Error(`unsupported MCP image type: ${block.mimeType}`);
82
- media.push({ type: "image", url: `data:${block.mimeType};base64,${block.data}` });
83
- } else if (block.type === "text" || block.type === "resource_link" || block.type === "resource" && typeof block.resource?.text === "string") {
84
- content.push(block);
85
- } else throw new Error(`unsupported MCP content type: ${block.type}; evidence ${evidenceSequence}`);
109
+ const message = content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
110
+ return failure("mcp_tool_error", `MCP ${definition.name}: ${message || "Tool failed"}; evidence ${evidenceSequence}`, retained);
86
111
  }
87
112
  const presented = { content, ...response.structuredContent === void 0 ? {} : { structuredContent: response.structuredContent }, evidenceSequence };
88
113
  return toolOutput(project ? await project(presented, definition.name) : presented, media);
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Client, ClientOptions, Transport } from "@modelcontextprotocol/client";
2
- import type { Environment, PlacedTool } from "@aexhq/brain";
2
+ import type { Environment, HostToolCall, PlacedTool } from "@aexhq/brain";
3
3
  export declare function connectMcp(transport: Transport, options?: ClientOptions & { name?: string; version?: string }): Promise<Client>;
4
4
  export interface McpPresentation { content: unknown[]; structuredContent?: unknown; evidenceSequence: number }
5
5
  export declare function mcpTools(options: { client: Client; names: string[]; env: Environment; prefix?: string;
6
+ publishMedia?: (media: { bytes: Uint8Array; mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "application/pdf"; index: number }, context: HostToolCall) => string | Promise<string>;
6
7
  project?: (output: McpPresentation, toolName: string) => unknown | Promise<unknown> }): Promise<PlacedTool[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/tools-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Selected MCP Tools in a Brain application host",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,6 +10,6 @@
10
10
  "exports": { ".": { "types": "./index.d.ts", "import": "./dist/index.mjs" }, "./package.json": "./package.json" },
11
11
  "files": ["dist", "index.d.ts", "README.md"],
12
12
  "scripts": { "build": "node ../../tools/build-environment.mjs", "test": "npm run build && node --test test/*.test.mjs", "test:journeys": "node --test test/journeys.mjs", "prepack": "npm run build" },
13
- "dependencies": { "@aexhq/brain": "0.22.0", "@modelcontextprotocol/client": "2.0.0", "zod": "4.4.3" },
13
+ "dependencies": { "@aexhq/brain": "0.24.0", "@modelcontextprotocol/client": "2.0.0", "zod": "4.4.3" },
14
14
  "devDependencies": { "@modelcontextprotocol/server": "2.0.0" }
15
15
  }