@aexhq/tools-mcp 0.0.0 → 0.1.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 ADDED
@@ -0,0 +1,40 @@
1
+ # @aexhq/tools-mcp
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.
4
+
5
+ ```js
6
+ import { hostEnv } from "@aexhq/brain";
7
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
8
+ import { connectMcp, mcpTools } from "@aexhq/tools-mcp";
9
+
10
+ const client = await connectMcp(new StdioClientTransport({
11
+ command: "node", args: ["./my-mcp-server.mjs"],
12
+ }), { versionNegotiation: { mode: { pin: "2026-07-28" } } });
13
+
14
+ const tools = await mcpTools({
15
+ client, env: hostEnv({ name: "app" }), names: ["search", "lookup"], prefix: "service_",
16
+ });
17
+ // Supply tools at session creation. Keep the client alive while these bindings are used.
18
+ // After ending their sessions:
19
+ // await client.close();
20
+ ```
21
+
22
+ The client SDK owns transport and protocol negotiation. `connectMcp` disables automatic interactive fulfilment; specify the protocol revision your server supports. A connected client can also be supplied directly. The bridge uses a fixed discovered Tool definition on every call, so an SDK cache refresh cannot change the session's schemas or trigger the header-mismatch resend path. Discovery uses the SDK's bounded pagination. Later list changes do not add or replace placed Tools; authorize a new session for changed definitions.
23
+
24
+ ## Results and failures
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.
27
+
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.
29
+
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.
31
+
32
+ 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
+
34
+ The original input JSON Schema is carried into Brain unchanged through Zod metadata. Host input parsing delegates to the MCP SDK's AJV validator without stripping properties, coercing values or inserting defaults. Conditional and dependent schemas, unevaluated properties, `$defs` and local recursive references are supported when accepted by both MCP and Brain validators. Invalid input fails as `invalid_input` before any MCP call. Unsupported dialects or unresolved references fail at construction; the bridge does not fetch external schemas. The MCP SDK validates structured output against the pinned original definition.
35
+
36
+ On application restart, reconnect the MCP client and recreate the same selected Tool bindings when reattaching the Brain host. This accepts future commands and never replays a disconnected invocation. Transport credentials remain application-owned; do not put them in Tool arguments or projection metadata.
37
+
38
+ ## Verification
39
+
40
+ `npm test --workspace @aexhq/tools-mcp` uses the real MCP v2 SDK and a separate stdio server process, including paginated discovery, cancellation, server termination after a mutation, structured results/errors, catalogue changes and images. Public-SDK journeys verify terminal outcomes, journal evidence, model presentation and original-schema validation through Brain, including that invalid calls produce no remote effects.
package/dist/index.mjs ADDED
@@ -0,0 +1,102 @@
1
+ // src/index.mjs
2
+ import { Client, ProtocolError, SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
3
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
4
+ import { inspectEnvironment, tool } from "@aexhq/brain";
5
+ import { z } from "zod";
6
+
7
+ // ../../shared/tool-output.mjs
8
+ function toolOutput(content, media = []) {
9
+ return { type: "aex_tool_output", version: 1, content, media };
10
+ }
11
+
12
+ // src/index.mjs
13
+ async function connectMcp(transport, { name = "aex-mcp", version = "0.1.0", ...options } = {}) {
14
+ const client = new Client({ name, version }, { ...options, inputRequired: { autoFulfill: false } });
15
+ try {
16
+ await client.connect(transport);
17
+ } catch (error) {
18
+ await client.close();
19
+ throw error;
20
+ }
21
+ return client;
22
+ }
23
+ async function mcpTools({ client, names, env, prefix = "", project }) {
24
+ if (inspectEnvironment(env).driver.driver !== "host") throw new TypeError("MCP Tools must be placed in hostEnv");
25
+ z.array(z.string().min(1)).min(1).parse(names);
26
+ if (new Set(names).size !== names.length) throw new TypeError("select each MCP Tool once");
27
+ const discovered = await client.listTools(void 0, { cacheMode: "refresh" });
28
+ const selected = names.map((name) => {
29
+ const matches = discovered.tools.filter((definition2) => definition2.name === name);
30
+ if (matches.length !== 1) throw new Error(`expected one MCP definition for ${name}, received ${matches.length}`);
31
+ const definition = structuredClone(matches[0]);
32
+ if (definition.execution?.taskSupport === "required") throw new Error(`${name} requires unsupported MCP task execution`);
33
+ return definition;
34
+ });
35
+ return selected.map((definition) => {
36
+ const validate = new AjvJsonSchemaValidator().getValidator(definition.inputSchema);
37
+ const input = z.unknown().superRefine((value, context) => {
38
+ const result = validate(value);
39
+ if (!result.valid) context.addIssue({ code: "custom", message: result.errorMessage });
40
+ }).meta(definition.inputSchema);
41
+ return tool({
42
+ name: `${prefix}${definition.name}`,
43
+ description: definition.description || definition.name,
44
+ input,
45
+ run: async (arguments_, context) => {
46
+ context.signal.throwIfAborted();
47
+ let response;
48
+ try {
49
+ response = await client.callTool({ name: definition.name, arguments: arguments_ }, {
50
+ signal: context.signal,
51
+ timeout: Math.max(1, context.deadline.getTime() - Date.now()),
52
+ toolDefinition: definition,
53
+ allowInputRequired: true
54
+ });
55
+ } catch (error) {
56
+ if (context.signal.aborted) throw error;
57
+ const message = String(error.message ?? error);
58
+ const details = { code: error.code ?? null, data: error.data ?? null };
59
+ const outcome = error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout ? { status: "timeout" } : error instanceof ProtocolError ? failure("mcp_protocol_error", message, details) : error instanceof SdkError && ![SdkErrorCode.ConnectionClosed, SdkErrorCode.SendFailed].includes(error.code) ? failure("mcp_sdk_error", message, details) : { status: "unknown", message: `MCP ${definition.name}: outcome unknown; ${message}` };
60
+ await context.emit("mcp_failure", {
61
+ tool: definition.name,
62
+ outcome: outcome.status,
63
+ code: details.code,
64
+ message,
65
+ details: details.data
66
+ });
67
+ return outcome;
68
+ }
69
+ const evidenceSequence = await context.emit("mcp_result", { tool: definition.name, result: response });
70
+ if (response.resultType && response.resultType !== "complete") {
71
+ return failure("mcp_unsupported_result", `MCP ${definition.name} requires unsupported continuation (${response.resultType}); evidence ${evidenceSequence}`, response);
72
+ }
73
+ 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}`);
86
+ }
87
+ const presented = { content, ...response.structuredContent === void 0 ? {} : { structuredContent: response.structuredContent }, evidenceSequence };
88
+ return toolOutput(project ? await project(presented, definition.name) : presented, media);
89
+ }
90
+ })({ env });
91
+ });
92
+ }
93
+ var failure = (code, message, details) => ({ status: "error", error: {
94
+ code,
95
+ message: message.slice(0, 4096),
96
+ retryable: false,
97
+ details
98
+ } });
99
+ export {
100
+ connectMcp,
101
+ mcpTools
102
+ };
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Client, ClientOptions, Transport } from "@modelcontextprotocol/client";
2
+ import type { Environment, PlacedTool } from "@aexhq/brain";
3
+ export declare function connectMcp(transport: Transport, options?: ClientOptions & { name?: string; version?: string }): Promise<Client>;
4
+ export interface McpPresentation { content: unknown[]; structuredContent?: unknown; evidenceSequence: number }
5
+ export declare function mcpTools(options: { client: Client; names: string[]; env: Environment; prefix?: string;
6
+ project?: (output: McpPresentation, toolName: string) => unknown | Promise<unknown> }): Promise<PlacedTool[]>;
package/package.json CHANGED
@@ -1,19 +1,15 @@
1
1
  {
2
2
  "name": "@aexhq/tools-mcp",
3
- "version": "0.0.0",
4
- "description": "Reserved package name; no stable release is available.",
5
- "license": "Apache-2.0",
3
+ "version": "0.1.0",
4
+ "description": "Selected MCP Tools in a Brain application host",
5
+ "license": "MIT",
6
6
  "type": "module",
7
- "exports": "./index.js",
8
- "files": [
9
- "index.js"
10
- ],
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/aexhq/extensions.git"
14
- },
15
- "aexHoldPlaceholder": {
16
- "stagedVersion": "0.1.0",
17
- "source": "4b36218cbe13166f8fcb5db3af8a34f13c99233a"
18
- }
7
+ "engines": { "node": ">=22.0.0" },
8
+ "repository": { "type": "git", "url": "git+https://github.com/aexhq/extensions.git", "directory": "packages/tools-mcp" },
9
+ "publishConfig": { "access": "public", "provenance": true, "tag": "next" },
10
+ "exports": { ".": { "types": "./index.d.ts", "import": "./dist/index.mjs" }, "./package.json": "./package.json" },
11
+ "files": ["dist", "index.d.ts", "README.md"],
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" },
14
+ "devDependencies": { "@modelcontextprotocol/server": "2.0.0" }
19
15
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- throw new Error("@aexhq/tools-mcp has no stable release; install @aexhq/tools-mcp@next or an exact version");