@agent-native/core 0.17.2 → 0.18.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.
- package/dist/action.d.ts +27 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +2 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +4 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +16 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +583 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/db/client.d.ts +17 -14
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +31 -27
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/build-server.d.ts +152 -0
- package/dist/mcp/build-server.d.ts.map +1 -0
- package/dist/mcp/build-server.js +349 -0
- package/dist/mcp/build-server.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +39 -0
- package/dist/mcp/builtin-tools.d.ts.map +1 -0
- package/dist/mcp/builtin-tools.js +401 -0
- package/dist/mcp/builtin-tools.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -13
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +44 -179
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts +44 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +209 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/workspace-resolve.d.ts +68 -0
- package/dist/mcp/workspace-resolve.d.ts.map +1 -0
- package/dist/mcp/workspace-resolve.js +205 -0
- package/dist/mcp/workspace-resolve.js.map +1 -0
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +3 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +1 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +9 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +71 -19
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +15 -10
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +9 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/deep-link.d.ts +55 -0
- package/dist/server/deep-link.d.ts.map +1 -0
- package/dist/server/deep-link.js +69 -0
- package/dist/server/deep-link.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/open-route.d.ts +12 -0
- package/dist/server/open-route.d.ts.map +1 -0
- package/dist/server/open-route.js +159 -0
- package/dist/server/open-route.js.map +1 -0
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/docs/content/external-agents.md +177 -0
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,180 +1,11 @@
|
|
|
1
|
-
import * as jose from "jose";
|
|
2
1
|
import { getH3App } from "../server/framework-request-handler.js";
|
|
3
2
|
import { defineEventHandler, setResponseStatus, getMethod, getRequestHeader, } from "h3";
|
|
4
3
|
import { readBody } from "../server/h3-helpers.js";
|
|
5
|
-
import {
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
const single = process.env.ACCESS_TOKEN;
|
|
11
|
-
const multi = process.env.ACCESS_TOKENS;
|
|
12
|
-
const tokens = [];
|
|
13
|
-
if (single)
|
|
14
|
-
tokens.push(single);
|
|
15
|
-
if (multi) {
|
|
16
|
-
tokens.push(...multi
|
|
17
|
-
.split(",")
|
|
18
|
-
.map((t) => t.trim())
|
|
19
|
-
.filter(Boolean));
|
|
20
|
-
}
|
|
21
|
-
return tokens;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Verify the inbound auth header. Returns:
|
|
25
|
-
* - { authed: true, identity } when verified — `identity` may be empty
|
|
26
|
-
* when authed via a static ACCESS_TOKEN (no caller email available).
|
|
27
|
-
* - { authed: false } on rejection.
|
|
28
|
-
*
|
|
29
|
-
* When A2A_SECRET is set we extract the JWT's `sub` (caller email) and
|
|
30
|
-
* `org_domain` claims so the MCP endpoint can wrap tool runs in
|
|
31
|
-
* `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the
|
|
32
|
-
* MCP endpoint loses tenant identity and downstream `accessFilter` /
|
|
33
|
-
* `resolveCredential` calls fall back to platform-wide defaults.
|
|
34
|
-
*/
|
|
35
|
-
async function verifyAuth(authHeader) {
|
|
36
|
-
// No auth configured → allow (dev mode), but no identity to propagate.
|
|
37
|
-
const accessTokens = getAccessTokens();
|
|
38
|
-
const hasA2ASecret = !!process.env.A2A_SECRET;
|
|
39
|
-
if (accessTokens.length === 0 && !hasA2ASecret) {
|
|
40
|
-
return { authed: true };
|
|
41
|
-
}
|
|
42
|
-
if (!authHeader?.startsWith("Bearer "))
|
|
43
|
-
return { authed: false };
|
|
44
|
-
const token = authHeader.slice(7);
|
|
45
|
-
// Try JWT via A2A_SECRET
|
|
46
|
-
if (hasA2ASecret) {
|
|
47
|
-
try {
|
|
48
|
-
const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(process.env.A2A_SECRET));
|
|
49
|
-
return {
|
|
50
|
-
authed: true,
|
|
51
|
-
identity: {
|
|
52
|
-
userEmail: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
53
|
-
orgDomain: typeof payload.org_domain === "string"
|
|
54
|
-
? payload.org_domain
|
|
55
|
-
: undefined,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// Not a valid JWT — fall through to token check
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Try ACCESS_TOKEN / ACCESS_TOKENS exact match (no per-caller identity).
|
|
64
|
-
if (accessTokens.length > 0 && accessTokens.includes(token)) {
|
|
65
|
-
return { authed: true };
|
|
66
|
-
}
|
|
67
|
-
return { authed: false };
|
|
68
|
-
}
|
|
69
|
-
async function resolveOrgIdFromDomain(orgDomain) {
|
|
70
|
-
if (!orgDomain)
|
|
71
|
-
return undefined;
|
|
72
|
-
try {
|
|
73
|
-
const { resolveOrgByDomain } = await import("../org/context.js");
|
|
74
|
-
const org = await resolveOrgByDomain(orgDomain);
|
|
75
|
-
return org?.orgId ?? undefined;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// MCP Server creation — converts ActionEntry registry to MCP tools
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
async function createMCPServerForRequest(config, identity) {
|
|
85
|
-
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
86
|
-
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
87
|
-
const server = new Server({ name: config.name, version: config.version ?? "1.0.0" }, { capabilities: { tools: {} } });
|
|
88
|
-
// Resolve orgId once per request (DB lookup) so subsequent wraps are
|
|
89
|
-
// synchronous. The caller identity may be undefined for ACCESS_TOKEN
|
|
90
|
-
// auth — in that case we run with no userEmail/orgId, which makes
|
|
91
|
-
// downstream tools that require per-user scope return empty results
|
|
92
|
-
// rather than cross-tenant data (the safe default).
|
|
93
|
-
const orgIdPromise = resolveOrgIdFromDomain(identity?.orgDomain);
|
|
94
|
-
/**
|
|
95
|
-
* Wrap a callback in `runWithRequestContext({ userEmail, orgId }, fn)`.
|
|
96
|
-
* Both the tools/list and tools/call handlers go through this so
|
|
97
|
-
* downstream `accessFilter`, `resolveCredential`, and per-user MCP
|
|
98
|
-
* visibility checks see the verified caller's identity.
|
|
99
|
-
*/
|
|
100
|
-
async function withCallerContext(fn) {
|
|
101
|
-
const orgId = await orgIdPromise;
|
|
102
|
-
return runWithRequestContext({ userEmail: identity?.userEmail, orgId }, fn);
|
|
103
|
-
}
|
|
104
|
-
// tools/list — return all actions + ask-agent meta-tool. Wrapped in the
|
|
105
|
-
// request context so per-user MCP visibility (mcp-client/visibility.ts)
|
|
106
|
-
// applies to the listing too.
|
|
107
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
108
|
-
return withCallerContext(async () => {
|
|
109
|
-
const tools = Object.entries(config.actions).map(([name, entry]) => ({
|
|
110
|
-
name,
|
|
111
|
-
description: entry.tool.description ?? name,
|
|
112
|
-
inputSchema: entry.tool.parameters ?? {
|
|
113
|
-
type: "object",
|
|
114
|
-
properties: {},
|
|
115
|
-
},
|
|
116
|
-
}));
|
|
117
|
-
if (config.askAgent) {
|
|
118
|
-
tools.push({
|
|
119
|
-
name: "ask-agent",
|
|
120
|
-
description: "Send a natural-language message to the app's AI agent and get a response. " +
|
|
121
|
-
"Use this for complex, multi-step tasks that require the agent's reasoning " +
|
|
122
|
-
"and full context about the app.",
|
|
123
|
-
inputSchema: {
|
|
124
|
-
type: "object",
|
|
125
|
-
properties: {
|
|
126
|
-
message: {
|
|
127
|
-
type: "string",
|
|
128
|
-
description: "The message to send to the agent",
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
required: ["message"],
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
return { tools };
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
// tools/call — dispatch to action registry or ask-agent. Wrapped in the
|
|
139
|
-
// request context so the action's `run(args)` and `askAgent()` execute
|
|
140
|
-
// with the verified caller's identity, not the platform default.
|
|
141
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
142
|
-
return withCallerContext(async () => {
|
|
143
|
-
const { name, arguments: args } = request.params;
|
|
144
|
-
if (name === "ask-agent" && config.askAgent) {
|
|
145
|
-
const message = args?.message ?? "";
|
|
146
|
-
try {
|
|
147
|
-
const result = await config.askAgent(message);
|
|
148
|
-
return { content: [{ type: "text", text: result }] };
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
return {
|
|
152
|
-
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
153
|
-
isError: true,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const entry = config.actions[name];
|
|
158
|
-
if (!entry) {
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
161
|
-
isError: true,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
const result = await entry.run(args ?? {});
|
|
166
|
-
return { content: [{ type: "text", text: result }] };
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
171
|
-
isError: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
return server;
|
|
177
|
-
}
|
|
4
|
+
import { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, } from "./build-server.js";
|
|
5
|
+
// Re-export the shared MCP server builder + types so the stdio transport and
|
|
6
|
+
// any (future) external importer of `@agent-native/core/mcp` keep resolving
|
|
7
|
+
// against `./server.js` exactly as before this refactor.
|
|
8
|
+
export { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, };
|
|
178
9
|
// ---------------------------------------------------------------------------
|
|
179
10
|
// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro
|
|
180
11
|
// ---------------------------------------------------------------------------
|
|
@@ -200,10 +31,14 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
200
31
|
return;
|
|
201
32
|
}
|
|
202
33
|
const method = getMethod(event);
|
|
203
|
-
// Auth check —
|
|
204
|
-
//
|
|
34
|
+
// Auth check — extracts the caller's identity from the JWT (`sub`),
|
|
35
|
+
// or, on the static-token / dev-open path, from the forwarded
|
|
36
|
+
// `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the
|
|
37
|
+
// `agent-native mcp install` flow). Without this the install flow
|
|
38
|
+
// would run every tool unscoped (userEmail === undefined).
|
|
205
39
|
const authHeader = getRequestHeader(event, "authorization");
|
|
206
|
-
const
|
|
40
|
+
const ownerEmailHeader = getRequestHeader(event, "x-agent-native-owner-email");
|
|
41
|
+
const authResult = await verifyAuth(authHeader, ownerEmailHeader);
|
|
207
42
|
if (!authResult.authed) {
|
|
208
43
|
setResponseStatus(event, 401);
|
|
209
44
|
return { error: "Unauthorized" };
|
|
@@ -228,7 +63,22 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
228
63
|
const transport = new StreamableHTTPServerTransport({
|
|
229
64
|
sessionIdGenerator: undefined, // stateless
|
|
230
65
|
});
|
|
231
|
-
|
|
66
|
+
// Derive the running app's origin so relative deep links become
|
|
67
|
+
// absolute URLs the external agent can open (same approach as A2A).
|
|
68
|
+
const forwardedProto = getRequestHeader(event, "x-forwarded-proto");
|
|
69
|
+
const host = getRequestHeader(event, "host");
|
|
70
|
+
const proto = forwardedProto?.split(",")[0]?.trim() ||
|
|
71
|
+
(host && /^(localhost|127\.0\.0\.1)(:|$)/.test(host)
|
|
72
|
+
? "http"
|
|
73
|
+
: "https");
|
|
74
|
+
const origin = host ? `${proto}://${host}` : undefined;
|
|
75
|
+
const targetHeader = getRequestHeader(event, "x-agent-native-open-target")?.toLowerCase();
|
|
76
|
+
const target = targetHeader === "desktop" ||
|
|
77
|
+
targetHeader === "terminal" ||
|
|
78
|
+
targetHeader === "browser"
|
|
79
|
+
? targetHeader
|
|
80
|
+
: undefined;
|
|
81
|
+
const server = await createMCPServerForRequest(config, authResult.identity, { origin, target });
|
|
232
82
|
await server.connect(transport);
|
|
233
83
|
// Delegate to the transport — it writes directly to the Node response.
|
|
234
84
|
// MCP's HTTP transport requires Node streams; this route is Node-only.
|
|
@@ -238,7 +88,22 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
238
88
|
setResponseStatus(event, 501);
|
|
239
89
|
return { error: "MCP requires Node runtime" };
|
|
240
90
|
}
|
|
241
|
-
|
|
91
|
+
try {
|
|
92
|
+
await transport.handleRequest(nodeReq, nodeRes, body);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// The SDK transport writes directly to the Node response. If the
|
|
96
|
+
// socket is already closed/ended (client disconnected, or the host
|
|
97
|
+
// stream layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END
|
|
98
|
+
// *after* the MCP payload was already delivered correctly. Swallow
|
|
99
|
+
// that benign post-flush write so an external agent disconnecting
|
|
100
|
+
// mid-stream can never take down the server process; rethrow
|
|
101
|
+
// anything else.
|
|
102
|
+
if (err?.code !== "ERR_STREAM_WRITE_AFTER_END")
|
|
103
|
+
throw err;
|
|
104
|
+
if (process.env.DEBUG)
|
|
105
|
+
console.log("[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)");
|
|
106
|
+
}
|
|
242
107
|
// Prevent H3 from double-writing the response
|
|
243
108
|
event._handled = true;
|
|
244
109
|
}));
|
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import * as jose from \"jose\";\nimport { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport type { ActionEntry } from \"../agent/production-agent.js\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport { runWithRequestContext } from \"../server/request-context.js\";\n\nexport interface MCPConfig {\n /** App name shown in MCP server info */\n name: string;\n /** App description */\n description: string;\n /** Version string (default \"1.0.0\") */\n version?: string;\n /** Action registry — same as agent chat and A2A */\n actions: Record<string, ActionEntry>;\n /** Handler for the ask-agent meta-tool — runs the full agent loop */\n askAgent?: (message: string) => Promise<string>;\n}\n\n/**\n * Identity extracted from a verified MCP bearer token / JWT. Used to wrap\n * `entry.run()` and `config.askAgent()` calls in `runWithRequestContext`\n * so downstream tools (db-query, accessFilter, resolveCredential) honour\n * per-user / per-org scoping. Without this wrap the MCP endpoint would\n * silently bypass tenant isolation. See finding #6 in\n * /tmp/security-audit/12-mcp-a2a-agent.md.\n */\ninterface MCPCallerIdentity {\n userEmail: string | undefined;\n orgDomain: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Auth — reuses the same pattern as A2A (Bearer token or JWT)\n// ---------------------------------------------------------------------------\n\nfunction getAccessTokens(): string[] {\n const single = process.env.ACCESS_TOKEN;\n const multi = process.env.ACCESS_TOKENS;\n const tokens: string[] = [];\n if (single) tokens.push(single);\n if (multi) {\n tokens.push(\n ...multi\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean),\n );\n }\n return tokens;\n}\n\n/**\n * Verify the inbound auth header. Returns:\n * - { authed: true, identity } when verified — `identity` may be empty\n * when authed via a static ACCESS_TOKEN (no caller email available).\n * - { authed: false } on rejection.\n *\n * When A2A_SECRET is set we extract the JWT's `sub` (caller email) and\n * `org_domain` claims so the MCP endpoint can wrap tool runs in\n * `runWithRequestContext({ userEmail, orgId })`. Without that wrap, the\n * MCP endpoint loses tenant identity and downstream `accessFilter` /\n * `resolveCredential` calls fall back to platform-wide defaults.\n */\nasync function verifyAuth(\n authHeader: string | undefined,\n): Promise<{ authed: boolean; identity?: MCPCallerIdentity }> {\n // No auth configured → allow (dev mode), but no identity to propagate.\n const accessTokens = getAccessTokens();\n const hasA2ASecret = !!process.env.A2A_SECRET;\n if (accessTokens.length === 0 && !hasA2ASecret) {\n return { authed: true };\n }\n\n if (!authHeader?.startsWith(\"Bearer \")) return { authed: false };\n const token = authHeader.slice(7);\n\n // Try JWT via A2A_SECRET\n if (hasA2ASecret) {\n try {\n const { payload } = await jose.jwtVerify(\n token,\n new TextEncoder().encode(process.env.A2A_SECRET!),\n );\n return {\n authed: true,\n identity: {\n userEmail: typeof payload.sub === \"string\" ? payload.sub : undefined,\n orgDomain:\n typeof payload.org_domain === \"string\"\n ? (payload.org_domain as string)\n : undefined,\n },\n };\n } catch {\n // Not a valid JWT — fall through to token check\n }\n }\n\n // Try ACCESS_TOKEN / ACCESS_TOKENS exact match (no per-caller identity).\n if (accessTokens.length > 0 && accessTokens.includes(token)) {\n return { authed: true };\n }\n\n return { authed: false };\n}\n\nasync function resolveOrgIdFromDomain(\n orgDomain: string | undefined,\n): Promise<string | undefined> {\n if (!orgDomain) return undefined;\n try {\n const { resolveOrgByDomain } = await import(\"../org/context.js\");\n const org = await resolveOrgByDomain(orgDomain);\n return org?.orgId ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// MCP Server creation — converts ActionEntry registry to MCP tools\n// ---------------------------------------------------------------------------\n\nasync function createMCPServerForRequest(\n config: MCPConfig,\n identity: MCPCallerIdentity | undefined,\n) {\n const { Server } = await import(\"@modelcontextprotocol/sdk/server/index.js\");\n const { ListToolsRequestSchema, CallToolRequestSchema } =\n await import(\"@modelcontextprotocol/sdk/types.js\");\n\n const server = new Server(\n { name: config.name, version: config.version ?? \"1.0.0\" },\n { capabilities: { tools: {} } },\n );\n\n // Resolve orgId once per request (DB lookup) so subsequent wraps are\n // synchronous. The caller identity may be undefined for ACCESS_TOKEN\n // auth — in that case we run with no userEmail/orgId, which makes\n // downstream tools that require per-user scope return empty results\n // rather than cross-tenant data (the safe default).\n const orgIdPromise = resolveOrgIdFromDomain(identity?.orgDomain);\n\n /**\n * Wrap a callback in `runWithRequestContext({ userEmail, orgId }, fn)`.\n * Both the tools/list and tools/call handlers go through this so\n * downstream `accessFilter`, `resolveCredential`, and per-user MCP\n * visibility checks see the verified caller's identity.\n */\n async function withCallerContext<T>(fn: () => Promise<T>): Promise<T> {\n const orgId = await orgIdPromise;\n return runWithRequestContext(\n { userEmail: identity?.userEmail, orgId },\n fn,\n ) as Promise<T>;\n }\n\n // tools/list — return all actions + ask-agent meta-tool. Wrapped in the\n // request context so per-user MCP visibility (mcp-client/visibility.ts)\n // applies to the listing too.\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return withCallerContext(async () => {\n const tools = Object.entries(config.actions).map(([name, entry]) => ({\n name,\n description: entry.tool.description ?? name,\n inputSchema: entry.tool.parameters ?? {\n type: \"object\" as const,\n properties: {},\n },\n }));\n\n if (config.askAgent) {\n tools.push({\n name: \"ask-agent\",\n description:\n \"Send a natural-language message to the app's AI agent and get a response. \" +\n \"Use this for complex, multi-step tasks that require the agent's reasoning \" +\n \"and full context about the app.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n message: {\n type: \"string\",\n description: \"The message to send to the agent\",\n },\n },\n required: [\"message\"],\n },\n });\n }\n\n return { tools };\n });\n });\n\n // tools/call — dispatch to action registry or ask-agent. Wrapped in the\n // request context so the action's `run(args)` and `askAgent()` execute\n // with the verified caller's identity, not the platform default.\n server.setRequestHandler(CallToolRequestSchema, async (request: any) => {\n return withCallerContext(async () => {\n const { name, arguments: args } = request.params;\n\n if (name === \"ask-agent\" && config.askAgent) {\n const message = args?.message ?? \"\";\n try {\n const result = await config.askAgent(message);\n return { content: [{ type: \"text\", text: result }] };\n } catch (err: any) {\n return {\n content: [{ type: \"text\", text: `Error: ${err.message}` }],\n isError: true,\n };\n }\n }\n\n const entry = config.actions[name];\n if (!entry) {\n return {\n content: [{ type: \"text\", text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n try {\n const result = await entry.run((args as Record<string, string>) ?? {});\n return { content: [{ type: \"text\", text: result }] };\n } catch (err: any) {\n return {\n content: [{ type: \"text\", text: `Error: ${err.message}` }],\n isError: true,\n };\n }\n });\n });\n\n return server;\n}\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/*\n // handle their own requests instead of treating them as MCP protocol\n // traffic.\n return;\n }\n\n const method = getMethod(event);\n\n // Auth check — also extracts the caller's identity from the JWT so\n // downstream tools run inside `runWithRequestContext`.\n const authHeader = getRequestHeader(event, \"authorization\");\n const authResult = await verifyAuth(authHeader);\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method === \"GET\") {\n // SSE stream endpoint — not used in stateless mode but the SDK\n // handles it gracefully. Let it through for protocol compliance.\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body)\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Create per-request stateless transport + server\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n const server = await createMCPServerForRequest(\n config,\n authResult.identity,\n );\n await server.connect(transport);\n\n // Delegate to the transport — it writes directly to the Node response.\n // MCP's HTTP transport requires Node streams; this route is Node-only.\n const nodeReq =\n (event as any).node?.req ?? (event as any).req?.runtime?.node?.req;\n const nodeRes =\n (event as any).node?.res ?? (event as any).req?.runtime?.node?.res;\n if (!nodeReq || !nodeRes) {\n setResponseStatus(event, 501);\n return { error: \"MCP requires Node runtime\" };\n }\n await transport.handleRequest(nodeReq, nodeRes, body);\n\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n type MCPConfig,\n type MCPCallerIdentity,\n type MCPRequestMeta,\n} from \"./build-server.js\";\n\n// Re-export the shared MCP server builder + types so the stdio transport and\n// any (future) external importer of `@agent-native/core/mcp` keep resolving\n// against `./server.js` exactly as before this refactor.\nexport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n};\nexport type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/*\n // handle their own requests instead of treating them as MCP protocol\n // traffic.\n return;\n }\n\n const method = getMethod(event);\n\n // Auth check — extracts the caller's identity from the JWT (`sub`),\n // or, on the static-token / dev-open path, from the forwarded\n // `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the\n // `agent-native mcp install` flow). Without this the install flow\n // would run every tool unscoped (userEmail === undefined).\n const authHeader = getRequestHeader(event, \"authorization\");\n const ownerEmailHeader = getRequestHeader(\n event,\n \"x-agent-native-owner-email\",\n );\n const authResult = await verifyAuth(authHeader, ownerEmailHeader);\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method === \"GET\") {\n // SSE stream endpoint — not used in stateless mode but the SDK\n // handles it gracefully. Let it through for protocol compliance.\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body)\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Create per-request stateless transport + server\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n // Derive the running app's origin so relative deep links become\n // absolute URLs the external agent can open (same approach as A2A).\n const forwardedProto = getRequestHeader(event, \"x-forwarded-proto\");\n const host = getRequestHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host)\n ? \"http\"\n : \"https\");\n const origin = host ? `${proto}://${host}` : undefined;\n const targetHeader = getRequestHeader(\n event,\n \"x-agent-native-open-target\",\n )?.toLowerCase();\n const target =\n targetHeader === \"desktop\" ||\n targetHeader === \"terminal\" ||\n targetHeader === \"browser\"\n ? (targetHeader as MCPRequestMeta[\"target\"])\n : undefined;\n\n const server = await createMCPServerForRequest(\n config,\n authResult.identity,\n { origin, target },\n );\n await server.connect(transport);\n\n // Delegate to the transport — it writes directly to the Node response.\n // MCP's HTTP transport requires Node streams; this route is Node-only.\n const nodeReq =\n (event as any).node?.req ?? (event as any).req?.runtime?.node?.req;\n const nodeRes =\n (event as any).node?.res ?? (event as any).req?.runtime?.node?.res;\n if (!nodeReq || !nodeRes) {\n setResponseStatus(event, 501);\n return { error: \"MCP requires Node runtime\" };\n }\n try {\n await transport.handleRequest(nodeReq, nodeRes, body);\n } catch (err: any) {\n // The SDK transport writes directly to the Node response. If the\n // socket is already closed/ended (client disconnected, or the host\n // stream layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END\n // *after* the MCP payload was already delivered correctly. Swallow\n // that benign post-flush write so an external agent disconnecting\n // mid-stream can never take down the server process; rethrow\n // anything else.\n if (err?.code !== \"ERR_STREAM_WRITE_AFTER_END\") throw err;\n if (process.env.DEBUG)\n console.log(\n \"[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)\",\n );\n }\n\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP **stdio** transport for the `agent-native mcp serve` command.
|
|
3
|
+
*
|
|
4
|
+
* This is the binary external coding agents (Claude Code, Claude Cowork,
|
|
5
|
+
* Codex) actually launch — they speak MCP over a child process's stdio, not
|
|
6
|
+
* HTTP. We expose the agent-native app's MCP surface over stdio in two modes:
|
|
7
|
+
*
|
|
8
|
+
* - **proxy (default)** — connect an MCP `Client` over
|
|
9
|
+
* `StreamableHTTPClientTransport` to the *already-running* local app's
|
|
10
|
+
* `http://127.0.0.1:<port>/_agent-native/mcp`, and run a stdio `Server`
|
|
11
|
+
* that forwards `tools/list` + `tools/call` to it. The live app is the
|
|
12
|
+
* single source of truth: HMR'd actions, the real registry, correct
|
|
13
|
+
* per-request deep links, and tenant scoping all come for free. If the
|
|
14
|
+
* app isn't running, we wait briefly for it (the workspace gateway boots
|
|
15
|
+
* it lazily on first request).
|
|
16
|
+
*
|
|
17
|
+
* - **standalone (`--standalone`)** — no running server, no HMR. Build the
|
|
18
|
+
* MCP server in-process from `autoDiscoverActions(cwd)` +
|
|
19
|
+
* `createMCPServerForRequest`, connected straight to a
|
|
20
|
+
* `StdioServerTransport`. Useful in CI / when nothing is serving.
|
|
21
|
+
*
|
|
22
|
+
* Node-only: imports `node:*` and the SDK stdio/http transports. Never part
|
|
23
|
+
* of the serverless bundle.
|
|
24
|
+
*/
|
|
25
|
+
export interface RunMCPStdioOptions {
|
|
26
|
+
/** App id to bridge to (workspace). Optional in a single-app project. */
|
|
27
|
+
appId?: string;
|
|
28
|
+
/** Explicit port of the running app's dev server. Overrides discovery. */
|
|
29
|
+
port?: number;
|
|
30
|
+
/** Skip the HTTP proxy and build the server in-process from disk. */
|
|
31
|
+
standalone?: boolean;
|
|
32
|
+
/** Working directory (defaults to process.cwd()). */
|
|
33
|
+
cwd?: string;
|
|
34
|
+
/** Env (defaults to process.env). */
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
/** Max ms to wait for the running app before failing (proxy mode). */
|
|
37
|
+
waitForAppMs?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Entry point for `agent-native mcp serve`. Defaults to proxy mode; pass
|
|
41
|
+
* `standalone: true` to build the server from disk with no running app.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runMCPStdio(opts?: RunMCPStdioOptions): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAqMD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAef"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP **stdio** transport for the `agent-native mcp serve` command.
|
|
3
|
+
*
|
|
4
|
+
* This is the binary external coding agents (Claude Code, Claude Cowork,
|
|
5
|
+
* Codex) actually launch — they speak MCP over a child process's stdio, not
|
|
6
|
+
* HTTP. We expose the agent-native app's MCP surface over stdio in two modes:
|
|
7
|
+
*
|
|
8
|
+
* - **proxy (default)** — connect an MCP `Client` over
|
|
9
|
+
* `StreamableHTTPClientTransport` to the *already-running* local app's
|
|
10
|
+
* `http://127.0.0.1:<port>/_agent-native/mcp`, and run a stdio `Server`
|
|
11
|
+
* that forwards `tools/list` + `tools/call` to it. The live app is the
|
|
12
|
+
* single source of truth: HMR'd actions, the real registry, correct
|
|
13
|
+
* per-request deep links, and tenant scoping all come for free. If the
|
|
14
|
+
* app isn't running, we wait briefly for it (the workspace gateway boots
|
|
15
|
+
* it lazily on first request).
|
|
16
|
+
*
|
|
17
|
+
* - **standalone (`--standalone`)** — no running server, no HMR. Build the
|
|
18
|
+
* MCP server in-process from `autoDiscoverActions(cwd)` +
|
|
19
|
+
* `createMCPServerForRequest`, connected straight to a
|
|
20
|
+
* `StdioServerTransport`. Useful in CI / when nothing is serving.
|
|
21
|
+
*
|
|
22
|
+
* Node-only: imports `node:*` and the SDK stdio/http transports. Never part
|
|
23
|
+
* of the serverless bundle.
|
|
24
|
+
*/
|
|
25
|
+
import { resolveLocalAppOrigin } from "./workspace-resolve.js";
|
|
26
|
+
const MCP_SUBPATH = "/_agent-native/mcp";
|
|
27
|
+
function log(msg) {
|
|
28
|
+
// stderr only — stdout is the MCP protocol channel and must stay clean.
|
|
29
|
+
process.stderr.write(`[mcp] ${msg}\n`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Owner identity the installer wrote into the client config's env. Passed
|
|
33
|
+
* through to the HTTP MCP endpoint as a JWT/identity bearer (when present)
|
|
34
|
+
* so tool runs stay tenant-scoped. For local dev with a static ACCESS_TOKEN
|
|
35
|
+
* the email is informational; for hosted JWT auth the token already carries
|
|
36
|
+
* `sub`, so we only add an `X-Agent-Native-Owner-Email` hint header.
|
|
37
|
+
*/
|
|
38
|
+
function authHeaders(env) {
|
|
39
|
+
const headers = {};
|
|
40
|
+
const token = env.ACCESS_TOKEN || env.AGENT_NATIVE_MCP_TOKEN;
|
|
41
|
+
if (token)
|
|
42
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
43
|
+
const owner = env.AGENT_NATIVE_OWNER_EMAIL;
|
|
44
|
+
if (owner)
|
|
45
|
+
headers["X-Agent-Native-Owner-Email"] = owner;
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
async function probeOrigin(origin, timeoutMs = 800) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${origin}${MCP_SUBPATH}`, {
|
|
51
|
+
method: "GET",
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
53
|
+
});
|
|
54
|
+
// Any HTTP response (even 401/405/406) means the server is up.
|
|
55
|
+
return res.status > 0;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Proxy mode: stdio Server ⇄ HTTP Client to the running app.
|
|
63
|
+
*
|
|
64
|
+
* We register the standard `tools/list` and `tools/call` handlers on the
|
|
65
|
+
* stdio server and forward them verbatim to the upstream HTTP MCP server via
|
|
66
|
+
* the SDK `Client`. The upstream owns tool definitions, results, and the
|
|
67
|
+
* appended deep-link block / `_meta`, so nothing is duplicated here.
|
|
68
|
+
*/
|
|
69
|
+
async function runProxy(opts) {
|
|
70
|
+
const { origin, appId } = await resolveLocalAppOrigin({
|
|
71
|
+
cwd: opts.cwd,
|
|
72
|
+
env: opts.env,
|
|
73
|
+
appId: opts.appId,
|
|
74
|
+
port: opts.port,
|
|
75
|
+
});
|
|
76
|
+
const env = opts.env ?? process.env;
|
|
77
|
+
const target = `${origin}${MCP_SUBPATH}`;
|
|
78
|
+
// Wait for the app to come up. The workspace gateway lazily boots an app's
|
|
79
|
+
// dev server on first request, so a fresh `mcp serve` may briefly race the
|
|
80
|
+
// boot. Hit the gateway path too so the lazy start is triggered.
|
|
81
|
+
const deadline = Date.now() + (opts.waitForAppMs ?? 60_000);
|
|
82
|
+
let up = await probeOrigin(origin);
|
|
83
|
+
if (!up) {
|
|
84
|
+
log(`Waiting for ${appId} at ${origin} …`);
|
|
85
|
+
while (!up && Date.now() < deadline) {
|
|
86
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
87
|
+
up = await probeOrigin(origin);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!up) {
|
|
91
|
+
throw new Error(`Timed out waiting for the local app at ${origin}. Start it with ` +
|
|
92
|
+
`\`agent-native dev\` (or \`agent-native workspace-dev\`), or run ` +
|
|
93
|
+
`\`agent-native mcp serve --standalone\` to build the server from disk.`);
|
|
94
|
+
}
|
|
95
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
96
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
97
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
98
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
99
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
100
|
+
// --- Upstream HTTP client -------------------------------------------------
|
|
101
|
+
const clientTransport = new StreamableHTTPClientTransport(new URL(target), {
|
|
102
|
+
requestInit: { headers: authHeaders(env) },
|
|
103
|
+
});
|
|
104
|
+
const client = new Client({ name: "agent-native-mcp-proxy", version: "1.0.0" }, { capabilities: {} });
|
|
105
|
+
await client.connect(clientTransport);
|
|
106
|
+
log(`Proxying stdio ⇄ ${target} (app: ${appId})`);
|
|
107
|
+
// --- Downstream stdio server ---------------------------------------------
|
|
108
|
+
const server = new Server({ name: `agent-native-${appId}`, version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
109
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
110
|
+
return client.listTools(request.params);
|
|
111
|
+
});
|
|
112
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
113
|
+
// Forward the call verbatim; the upstream appends the deep-link block.
|
|
114
|
+
return client.callTool(request.params);
|
|
115
|
+
});
|
|
116
|
+
const stdioTransport = new StdioServerTransport();
|
|
117
|
+
await server.connect(stdioTransport);
|
|
118
|
+
// Keep the proxy alive until the client/transport closes.
|
|
119
|
+
await new Promise((resolve) => {
|
|
120
|
+
const done = () => resolve();
|
|
121
|
+
stdioTransport.onclose = done;
|
|
122
|
+
clientTransport.onclose = done;
|
|
123
|
+
process.once("SIGINT", done);
|
|
124
|
+
process.once("SIGTERM", done);
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
await client.close();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// best-effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Standalone mode: build the MCP server in-process from disk.
|
|
135
|
+
*
|
|
136
|
+
* No running server, no HMR — actions are discovered via
|
|
137
|
+
* `autoDiscoverActions(cwd)` and the shared `createMCPServerForRequest`
|
|
138
|
+
* builder is reused so behavior (tools, deep links, builtin cross-app tools)
|
|
139
|
+
* matches the HTTP mount exactly.
|
|
140
|
+
*/
|
|
141
|
+
async function runStandalone(opts) {
|
|
142
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
143
|
+
const env = opts.env ?? process.env;
|
|
144
|
+
const { resolveLocalAppOrigin } = await import("./workspace-resolve.js");
|
|
145
|
+
let appId = opts.appId ?? "app";
|
|
146
|
+
let origin;
|
|
147
|
+
try {
|
|
148
|
+
const resolved = await resolveLocalAppOrigin({
|
|
149
|
+
cwd,
|
|
150
|
+
env,
|
|
151
|
+
appId: opts.appId,
|
|
152
|
+
port: opts.port,
|
|
153
|
+
});
|
|
154
|
+
appId = resolved.appId;
|
|
155
|
+
// Origin is best-effort here (server may not be running) — still useful
|
|
156
|
+
// so a `link` builder's relative deep link becomes an absolute URL.
|
|
157
|
+
origin = resolved.origin;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// No workspace / can't resolve — fall back to a bare app id.
|
|
161
|
+
}
|
|
162
|
+
const { autoDiscoverActions } = await import("../server/action-discovery.js");
|
|
163
|
+
const { createMCPServerForRequest } = await import("./build-server.js");
|
|
164
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
165
|
+
const actions = await autoDiscoverActions(cwd);
|
|
166
|
+
log(`Standalone: discovered ${Object.keys(actions).length} action(s) in ${cwd}`);
|
|
167
|
+
const server = await createMCPServerForRequest({
|
|
168
|
+
name: appId.charAt(0).toUpperCase() + appId.slice(1),
|
|
169
|
+
appId,
|
|
170
|
+
description: `Agent-native ${appId} app (standalone MCP)`,
|
|
171
|
+
actions,
|
|
172
|
+
// No askAgent in standalone — there is no running engine/runtime here.
|
|
173
|
+
// builtin cross-app tools stay on so `list_apps` / `open_app` /
|
|
174
|
+
// `create_workspace_app` / `list_templates` still work from disk.
|
|
175
|
+
},
|
|
176
|
+
// No verified identity in standalone (no inbound auth header). Runs with
|
|
177
|
+
// platform-default scope, same as a tokenless local HTTP mount.
|
|
178
|
+
undefined, { origin });
|
|
179
|
+
const transport = new StdioServerTransport();
|
|
180
|
+
await server.connect(transport);
|
|
181
|
+
await new Promise((resolve) => {
|
|
182
|
+
const done = () => resolve();
|
|
183
|
+
transport.onclose = done;
|
|
184
|
+
process.once("SIGINT", done);
|
|
185
|
+
process.once("SIGTERM", done);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Entry point for `agent-native mcp serve`. Defaults to proxy mode; pass
|
|
190
|
+
* `standalone: true` to build the server from disk with no running app.
|
|
191
|
+
*/
|
|
192
|
+
export async function runMCPStdio(opts = {}) {
|
|
193
|
+
if (opts.standalone) {
|
|
194
|
+
await runStandalone(opts);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await runProxy(opts);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
// Proxy couldn't reach a running app — surface a clear, actionable
|
|
202
|
+
// message on stderr. We do NOT silently fall back to standalone: the
|
|
203
|
+
// caller asked for the live registry; auto-falling-back would hide a
|
|
204
|
+
// broken dev server and serve stale tools.
|
|
205
|
+
log(`Proxy mode failed: ${err?.message ?? err}`);
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MCP **stdio** transport for the `agent-native mcp serve` command.\n *\n * This is the binary external coding agents (Claude Code, Claude Cowork,\n * Codex) actually launch — they speak MCP over a child process's stdio, not\n * HTTP. We expose the agent-native app's MCP surface over stdio in two modes:\n *\n * - **proxy (default)** — connect an MCP `Client` over\n * `StreamableHTTPClientTransport` to the *already-running* local app's\n * `http://127.0.0.1:<port>/_agent-native/mcp`, and run a stdio `Server`\n * that forwards `tools/list` + `tools/call` to it. The live app is the\n * single source of truth: HMR'd actions, the real registry, correct\n * per-request deep links, and tenant scoping all come for free. If the\n * app isn't running, we wait briefly for it (the workspace gateway boots\n * it lazily on first request).\n *\n * - **standalone (`--standalone`)** — no running server, no HMR. Build the\n * MCP server in-process from `autoDiscoverActions(cwd)` +\n * `createMCPServerForRequest`, connected straight to a\n * `StdioServerTransport`. Useful in CI / when nothing is serving.\n *\n * Node-only: imports `node:*` and the SDK stdio/http transports. Never part\n * of the serverless bundle.\n */\n\nimport { resolveLocalAppOrigin } from \"./workspace-resolve.js\";\n\nexport interface RunMCPStdioOptions {\n /** App id to bridge to (workspace). Optional in a single-app project. */\n appId?: string;\n /** Explicit port of the running app's dev server. Overrides discovery. */\n port?: number;\n /** Skip the HTTP proxy and build the server in-process from disk. */\n standalone?: boolean;\n /** Working directory (defaults to process.cwd()). */\n cwd?: string;\n /** Env (defaults to process.env). */\n env?: NodeJS.ProcessEnv;\n /** Max ms to wait for the running app before failing (proxy mode). */\n waitForAppMs?: number;\n}\n\nconst MCP_SUBPATH = \"/_agent-native/mcp\";\n\nfunction log(msg: string): void {\n // stderr only — stdout is the MCP protocol channel and must stay clean.\n process.stderr.write(`[mcp] ${msg}\\n`);\n}\n\n/**\n * Owner identity the installer wrote into the client config's env. Passed\n * through to the HTTP MCP endpoint as a JWT/identity bearer (when present)\n * so tool runs stay tenant-scoped. For local dev with a static ACCESS_TOKEN\n * the email is informational; for hosted JWT auth the token already carries\n * `sub`, so we only add an `X-Agent-Native-Owner-Email` hint header.\n */\nfunction authHeaders(env: NodeJS.ProcessEnv): Record<string, string> {\n const headers: Record<string, string> = {};\n const token = env.ACCESS_TOKEN || env.AGENT_NATIVE_MCP_TOKEN;\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\n const owner = env.AGENT_NATIVE_OWNER_EMAIL;\n if (owner) headers[\"X-Agent-Native-Owner-Email\"] = owner;\n return headers;\n}\n\nasync function probeOrigin(origin: string, timeoutMs = 800): Promise<boolean> {\n try {\n const res = await fetch(`${origin}${MCP_SUBPATH}`, {\n method: \"GET\",\n signal: AbortSignal.timeout(timeoutMs),\n });\n // Any HTTP response (even 401/405/406) means the server is up.\n return res.status > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Proxy mode: stdio Server ⇄ HTTP Client to the running app.\n *\n * We register the standard `tools/list` and `tools/call` handlers on the\n * stdio server and forward them verbatim to the upstream HTTP MCP server via\n * the SDK `Client`. The upstream owns tool definitions, results, and the\n * appended deep-link block / `_meta`, so nothing is duplicated here.\n */\nasync function runProxy(opts: RunMCPStdioOptions): Promise<void> {\n const { origin, appId } = await resolveLocalAppOrigin({\n cwd: opts.cwd,\n env: opts.env,\n appId: opts.appId,\n port: opts.port,\n });\n const env = opts.env ?? process.env;\n const target = `${origin}${MCP_SUBPATH}`;\n\n // Wait for the app to come up. The workspace gateway lazily boots an app's\n // dev server on first request, so a fresh `mcp serve` may briefly race the\n // boot. Hit the gateway path too so the lazy start is triggered.\n const deadline = Date.now() + (opts.waitForAppMs ?? 60_000);\n let up = await probeOrigin(origin);\n if (!up) {\n log(`Waiting for ${appId} at ${origin} …`);\n while (!up && Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 750));\n up = await probeOrigin(origin);\n }\n }\n if (!up) {\n throw new Error(\n `Timed out waiting for the local app at ${origin}. Start it with ` +\n `\\`agent-native dev\\` (or \\`agent-native workspace-dev\\`), or run ` +\n `\\`agent-native mcp serve --standalone\\` to build the server from disk.`,\n );\n }\n\n const { Client } = await import(\"@modelcontextprotocol/sdk/client/index.js\");\n const { StreamableHTTPClientTransport } =\n await import(\"@modelcontextprotocol/sdk/client/streamableHttp.js\");\n const { Server } = await import(\"@modelcontextprotocol/sdk/server/index.js\");\n const { StdioServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/stdio.js\");\n const { ListToolsRequestSchema, CallToolRequestSchema } =\n await import(\"@modelcontextprotocol/sdk/types.js\");\n\n // --- Upstream HTTP client -------------------------------------------------\n const clientTransport = new StreamableHTTPClientTransport(new URL(target), {\n requestInit: { headers: authHeaders(env) },\n });\n const client = new Client(\n { name: \"agent-native-mcp-proxy\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n await client.connect(clientTransport);\n log(`Proxying stdio ⇄ ${target} (app: ${appId})`);\n\n // --- Downstream stdio server ---------------------------------------------\n const server = new Server(\n { name: `agent-native-${appId}`, version: \"1.0.0\" },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async (request: any) => {\n return client.listTools(request.params);\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request: any) => {\n // Forward the call verbatim; the upstream appends the deep-link block.\n return client.callTool(request.params);\n });\n\n const stdioTransport = new StdioServerTransport();\n await server.connect(stdioTransport);\n\n // Keep the proxy alive until the client/transport closes.\n await new Promise<void>((resolve) => {\n const done = () => resolve();\n stdioTransport.onclose = done;\n clientTransport.onclose = done;\n process.once(\"SIGINT\", done);\n process.once(\"SIGTERM\", done);\n });\n\n try {\n await client.close();\n } catch {\n // best-effort\n }\n}\n\n/**\n * Standalone mode: build the MCP server in-process from disk.\n *\n * No running server, no HMR — actions are discovered via\n * `autoDiscoverActions(cwd)` and the shared `createMCPServerForRequest`\n * builder is reused so behavior (tools, deep links, builtin cross-app tools)\n * matches the HTTP mount exactly.\n */\nasync function runStandalone(opts: RunMCPStdioOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n const env = opts.env ?? process.env;\n\n const { resolveLocalAppOrigin } = await import(\"./workspace-resolve.js\");\n let appId = opts.appId ?? \"app\";\n let origin: string | undefined;\n try {\n const resolved = await resolveLocalAppOrigin({\n cwd,\n env,\n appId: opts.appId,\n port: opts.port,\n });\n appId = resolved.appId;\n // Origin is best-effort here (server may not be running) — still useful\n // so a `link` builder's relative deep link becomes an absolute URL.\n origin = resolved.origin;\n } catch {\n // No workspace / can't resolve — fall back to a bare app id.\n }\n\n const { autoDiscoverActions } = await import(\"../server/action-discovery.js\");\n const { createMCPServerForRequest } = await import(\"./build-server.js\");\n const { StdioServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/stdio.js\");\n\n const actions = await autoDiscoverActions(cwd);\n log(\n `Standalone: discovered ${Object.keys(actions).length} action(s) in ${cwd}`,\n );\n\n const server = await createMCPServerForRequest(\n {\n name: appId.charAt(0).toUpperCase() + appId.slice(1),\n appId,\n description: `Agent-native ${appId} app (standalone MCP)`,\n actions,\n // No askAgent in standalone — there is no running engine/runtime here.\n // builtin cross-app tools stay on so `list_apps` / `open_app` /\n // `create_workspace_app` / `list_templates` still work from disk.\n },\n // No verified identity in standalone (no inbound auth header). Runs with\n // platform-default scope, same as a tokenless local HTTP mount.\n undefined,\n { origin },\n );\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n await new Promise<void>((resolve) => {\n const done = () => resolve();\n transport.onclose = done;\n process.once(\"SIGINT\", done);\n process.once(\"SIGTERM\", done);\n });\n}\n\n/**\n * Entry point for `agent-native mcp serve`. Defaults to proxy mode; pass\n * `standalone: true` to build the server from disk with no running app.\n */\nexport async function runMCPStdio(\n opts: RunMCPStdioOptions = {},\n): Promise<void> {\n if (opts.standalone) {\n await runStandalone(opts);\n return;\n }\n try {\n await runProxy(opts);\n } catch (err: any) {\n // Proxy couldn't reach a running app — surface a clear, actionable\n // message on stderr. We do NOT silently fall back to standalone: the\n // caller asked for the live registry; auto-falling-back would hide a\n // broken dev server and serve stale tools.\n log(`Proxy mode failed: ${err?.message ?? err}`);\n throw err;\n }\n}\n"]}
|