@agent-native/core 0.18.1 → 0.19.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/README.md +1 -11
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +1 -1
- package/dist/a2a/caller-auth.js.map +1 -1
- package/dist/a2a/client.d.ts +7 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +3 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +34 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +47 -256
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/connect.d.ts +94 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +443 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +71 -0
- package/dist/cli/mcp-config-writers.d.ts.map +1 -0
- package/dist/cli/mcp-config-writers.js +210 -0
- package/dist/cli/mcp-config-writers.js.map +1 -0
- package/dist/client/AgentPanel.d.ts +3 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +3 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +22 -66
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +4 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +6 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +5 -4
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +6 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +20 -10
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/conversation/AgentConversation.d.ts +18 -0
- package/dist/client/conversation/AgentConversation.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.js +94 -0
- package/dist/client/conversation/AgentConversation.js.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts +2 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.js +69 -0
- package/dist/client/conversation/AgentConversation.spec.js.map +1 -0
- package/dist/client/conversation/index.d.ts +4 -0
- package/dist/client/conversation/index.d.ts.map +1 -0
- package/dist/client/conversation/index.js +3 -0
- package/dist/client/conversation/index.js.map +1 -0
- package/dist/client/conversation/types.d.ts +54 -0
- package/dist/client/conversation/types.d.ts.map +1 -0
- package/dist/client/conversation/types.js +2 -0
- package/dist/client/conversation/types.js.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +15 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js +66 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -0
- package/dist/client/dynamic-suggestions.d.ts +43 -0
- package/dist/client/dynamic-suggestions.d.ts.map +1 -0
- package/dist/client/dynamic-suggestions.js +344 -0
- package/dist/client/dynamic-suggestions.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +2 -2
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +4 -28
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +2 -2
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/code-agents/index.d.ts +1 -0
- package/dist/code-agents/index.d.ts.map +1 -1
- package/dist/code-agents/index.js +1 -0
- package/dist/code-agents/index.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.d.ts +50 -0
- package/dist/code-agents/transcript-normalizer.d.ts.map +1 -0
- package/dist/code-agents/transcript-normalizer.js +356 -0
- package/dist/code-agents/transcript-normalizer.js.map +1 -0
- package/dist/coding-tools/index.d.ts +31 -0
- package/dist/coding-tools/index.d.ts.map +1 -0
- package/dist/coding-tools/index.js +411 -0
- package/dist/coding-tools/index.js.map +1 -0
- package/dist/extensions/schema.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +30 -0
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +85 -26
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/connect-route.d.ts +43 -0
- package/dist/mcp/connect-route.d.ts.map +1 -0
- package/dist/mcp/connect-route.js +744 -0
- package/dist/mcp/connect-route.js.map +1 -0
- package/dist/mcp/connect-store.d.ts +132 -0
- package/dist/mcp/connect-store.d.ts.map +1 -0
- package/dist/mcp/connect-store.js +434 -0
- package/dist/mcp/connect-store.js.map +1 -0
- package/dist/mcp/org-directory.d.ts +83 -0
- package/dist/mcp/org-directory.d.ts.map +1 -0
- package/dist/mcp/org-directory.js +201 -0
- package/dist/mcp/org-directory.js.map +1 -0
- package/dist/mcp/server.d.ts +38 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +208 -77
- package/dist/mcp/server.js.map +1 -1
- package/dist/scripts/dev/index.d.ts +6 -4
- package/dist/scripts/dev/index.d.ts.map +1 -1
- package/dist/scripts/dev/index.js +28 -13
- package/dist/scripts/dev/index.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -6
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +32 -32
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-teams.js +2 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/agents-bundle.d.ts +3 -3
- package/dist/server/agents-bundle.js +5 -5
- package/dist/server/agents-bundle.js.map +1 -1
- package/dist/server/auth.d.ts +17 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +149 -33
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +43 -0
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +25 -0
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +12 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +42 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/identity-sso-store.d.ts +86 -0
- package/dist/server/identity-sso-store.d.ts.map +1 -0
- package/dist/server/identity-sso-store.js +243 -0
- package/dist/server/identity-sso-store.js.map +1 -0
- package/dist/server/identity-sso.d.ts +78 -0
- package/dist/server/identity-sso.d.ts.map +1 -0
- package/dist/server/identity-sso.js +425 -0
- package/dist/server/identity-sso.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/sentry.d.ts.map +1 -1
- package/dist/server/sentry.js +17 -2
- package/dist/server/sentry.js.map +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/docs/content/client.md +15 -0
- package/docs/content/code-agents-ui.md +25 -4
- package/docs/content/cross-app-sso.md +118 -0
- package/docs/content/drop-in-agent.md +3 -1
- package/docs/content/external-agents.md +130 -51
- package/docs/content/frames.md +1 -1
- package/docs/content/migration-workbench.md +6 -1
- package/package.json +2 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org-directory discovery for the generic cross-app MCP verbs
|
|
3
|
+
* (`list_apps` / `ask_app` in `builtin-tools.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Phase 3b of cross-app auto-wiring. Today the cross-app verbs resolve sibling
|
|
6
|
+
* apps from *local workspace* info only (`workspace-resolve.ts`), so the mail
|
|
7
|
+
* agent can only reach the calendar agent in a local dev workspace. When the
|
|
8
|
+
* deployment runs against an org directory (Dispatch is also the identity hub
|
|
9
|
+
* for the org), this module discovers the org's *deployed* sibling apps so the
|
|
10
|
+
* same verbs work cross-app in production with ZERO manual setup.
|
|
11
|
+
*
|
|
12
|
+
* ## The directory request
|
|
13
|
+
*
|
|
14
|
+
* GET <directoryOrigin>/_agent-native/org/apps
|
|
15
|
+
* Auth Authorization: Bearer <org A2A token> (same signed token A2A peers
|
|
16
|
+
* already mint — reuses `resolveA2ACallerAuth()`; the org A2A secret /
|
|
17
|
+
* global `A2A_SECRET` is loaded exactly how outgoing A2A calls load it)
|
|
18
|
+
* ⇒ { org, apps: [ { id, name, url, a2aUrl, capabilities? } ] }
|
|
19
|
+
* (allow-listed first-party apps only, prod URLs — enforced by the
|
|
20
|
+
* authority side, Phase 3a, on Dispatch)
|
|
21
|
+
*
|
|
22
|
+
* ## Resolution + safety model
|
|
23
|
+
*
|
|
24
|
+
* - The directory origin is read from env: `AGENT_NATIVE_ORG_DIRECTORY_URL`
|
|
25
|
+
* (dedicated) or `AGENT_NATIVE_IDENTITY_HUB_URL` (Dispatch is also the
|
|
26
|
+
* identity hub). When *neither* is set the feature is simply inactive —
|
|
27
|
+
* `fetchOrgApps()` returns `[]` and nothing changes anywhere (asserted by
|
|
28
|
+
* a test). This makes the whole feature opt-in and back-compat.
|
|
29
|
+
* - On ANY error (no env, unreachable, 401, non-2xx, bad JSON, no signed
|
|
30
|
+
* token) `fetchOrgApps()` returns `[]` and NEVER throws — the cross-app
|
|
31
|
+
* verbs degrade silently to their exact current local-only behavior.
|
|
32
|
+
* - A short in-memory TTL cache (default 60s) keyed by directory origin and
|
|
33
|
+
* caller identity/org scope so sibling app lists never cross tenants.
|
|
34
|
+
* Empty authenticated results are cached too (with a shorter TTL) so a
|
|
35
|
+
* transient failure doesn't hammer the directory on every call.
|
|
36
|
+
* - No secrets are ever logged.
|
|
37
|
+
*
|
|
38
|
+
* Bundled alongside `mountMCP` (no Node-only top-level imports). The A2A
|
|
39
|
+
* caller-auth + a2a client are dynamically imported inside `fetchOrgApps()`.
|
|
40
|
+
*/
|
|
41
|
+
/** Default cache TTL for a successful directory fetch. */
|
|
42
|
+
const SUCCESS_TTL_MS = 60_000;
|
|
43
|
+
/** Shorter TTL for an empty/failed fetch so transient errors recover fast. */
|
|
44
|
+
const EMPTY_TTL_MS = 10_000;
|
|
45
|
+
/** In-memory cache keyed by resolved directory origin (+ identity scope). */
|
|
46
|
+
const cache = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the org-directory origin from env. Returns `null` when neither env
|
|
49
|
+
* var is set — the caller treats `null` as "feature inactive".
|
|
50
|
+
*
|
|
51
|
+
* `env` is injectable for tests; defaults to `process.env`.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveOrgDirectoryOrigin(env = process.env) {
|
|
54
|
+
const raw = env.AGENT_NATIVE_ORG_DIRECTORY_URL || env.AGENT_NATIVE_IDENTITY_HUB_URL;
|
|
55
|
+
if (!raw || typeof raw !== "string")
|
|
56
|
+
return null;
|
|
57
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
// Validate it's an absolute http(s) URL; reject anything else.
|
|
62
|
+
const u = new URL(trimmed);
|
|
63
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
64
|
+
return null;
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function normalizeApp(raw) {
|
|
72
|
+
if (!raw || typeof raw !== "object")
|
|
73
|
+
return null;
|
|
74
|
+
const r = raw;
|
|
75
|
+
const id = typeof r.id === "string" ? r.id.trim().toLowerCase() : "";
|
|
76
|
+
const url = typeof r.url === "string" ? r.url.trim() : "";
|
|
77
|
+
if (!id || !url)
|
|
78
|
+
return null;
|
|
79
|
+
// Only accept absolute http(s) URLs from the directory.
|
|
80
|
+
try {
|
|
81
|
+
const u = new URL(url);
|
|
82
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const name = typeof r.name === "string" && r.name.trim() ? r.name.trim() : id;
|
|
89
|
+
const a2aUrl = typeof r.a2aUrl === "string" && r.a2aUrl.trim() ? r.a2aUrl.trim() : url;
|
|
90
|
+
const capabilities = Array.isArray(r.capabilities)
|
|
91
|
+
? r.capabilities.filter((c) => typeof c === "string")
|
|
92
|
+
: undefined;
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
name,
|
|
96
|
+
url: url.replace(/\/+$/, ""),
|
|
97
|
+
a2aUrl: a2aUrl.replace(/\/+$/, ""),
|
|
98
|
+
...(capabilities && capabilities.length ? { capabilities } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Compare two origins by host (ignores trailing slash / protocol noise). */
|
|
102
|
+
function sameOrigin(a, b) {
|
|
103
|
+
try {
|
|
104
|
+
const ua = new URL(a);
|
|
105
|
+
const ub = new URL(b);
|
|
106
|
+
return ua.host === ub.host && ua.protocol === ub.protocol;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function scopedCacheKey(origin, auth) {
|
|
113
|
+
return [
|
|
114
|
+
origin,
|
|
115
|
+
`user:${auth.userEmail ?? ""}`,
|
|
116
|
+
`org:${auth.orgId ?? auth.orgDomain ?? ""}`,
|
|
117
|
+
].join("|");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fetch the org's first-party sibling apps from the org directory.
|
|
121
|
+
*
|
|
122
|
+
* - Returns `[]` (never throws) on ANY failure or when the directory env is
|
|
123
|
+
* unset — the cross-app verbs then keep their exact local-only behavior.
|
|
124
|
+
* - Short in-memory TTL cache so it isn't fetched on every tool call.
|
|
125
|
+
* - Strips the current app from the result (compared by id and by origin) so
|
|
126
|
+
* `list_apps` / `ask_app` never offer to route to themselves.
|
|
127
|
+
*
|
|
128
|
+
* @param opts.selfId Current app id (so it's stripped from the result).
|
|
129
|
+
* @param opts.selfOrigin Current app origin (so it's stripped by origin too).
|
|
130
|
+
* @param opts.env Injectable env (tests). Defaults to `process.env`.
|
|
131
|
+
*/
|
|
132
|
+
export async function fetchOrgApps(opts) {
|
|
133
|
+
const env = opts?.env ?? process.env;
|
|
134
|
+
const origin = resolveOrgDirectoryOrigin(env);
|
|
135
|
+
// Feature inactive: no directory configured ⇒ behave exactly as before.
|
|
136
|
+
if (!origin)
|
|
137
|
+
return [];
|
|
138
|
+
const selfId = (opts?.selfId ?? "").trim().toLowerCase();
|
|
139
|
+
const selfOrigin = (opts?.selfOrigin ?? "").trim();
|
|
140
|
+
const stripSelf = (apps) => apps.filter((a) => {
|
|
141
|
+
if (selfId && a.id === selfId)
|
|
142
|
+
return false;
|
|
143
|
+
if (selfOrigin && sameOrigin(a.url, selfOrigin))
|
|
144
|
+
return false;
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
let cacheKey = null;
|
|
148
|
+
let apps = [];
|
|
149
|
+
let ttl = EMPTY_TTL_MS;
|
|
150
|
+
try {
|
|
151
|
+
// Reuse the existing A2A caller-auth: it reads userEmail + orgId from the
|
|
152
|
+
// request context, loads the org A2A secret via getOrgA2ASecret (falling
|
|
153
|
+
// back to the global A2A_SECRET env), and signs the same bearer JWT A2A
|
|
154
|
+
// peers already use. No new secret loading is invented here.
|
|
155
|
+
const { resolveA2ACallerAuth } = await import("../a2a/caller-auth.js");
|
|
156
|
+
const auth = await resolveA2ACallerAuth();
|
|
157
|
+
if (!auth.apiKey) {
|
|
158
|
+
// No signed token available (no A2A secret / no caller identity) — the
|
|
159
|
+
// directory requires the org bearer, so degrade silently to local-only.
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
cacheKey = scopedCacheKey(origin, auth);
|
|
164
|
+
const cached = cache.get(cacheKey);
|
|
165
|
+
if (cached && cached.expiresAt > now) {
|
|
166
|
+
return stripSelf(cached.apps);
|
|
167
|
+
}
|
|
168
|
+
const res = await fetch(`${origin}/_agent-native/org/apps`, {
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
|
172
|
+
Accept: "application/json",
|
|
173
|
+
},
|
|
174
|
+
signal: AbortSignal.timeout(4000),
|
|
175
|
+
});
|
|
176
|
+
if (res.ok) {
|
|
177
|
+
const json = (await res.json());
|
|
178
|
+
const list = Array.isArray(json?.apps) ? json.apps : [];
|
|
179
|
+
apps = list.map(normalizeApp).filter((a) => a !== null);
|
|
180
|
+
ttl = SUCCESS_TTL_MS;
|
|
181
|
+
}
|
|
182
|
+
// Non-2xx ⇒ leave apps=[] with the short EMPTY_TTL (silent degrade).
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Unreachable / parse error / abort ⇒ silent degrade to local-only.
|
|
186
|
+
apps = [];
|
|
187
|
+
ttl = EMPTY_TTL_MS;
|
|
188
|
+
}
|
|
189
|
+
if (cacheKey) {
|
|
190
|
+
cache.set(cacheKey, {
|
|
191
|
+
apps,
|
|
192
|
+
expiresAt: Date.now() + ttl,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return stripSelf(apps);
|
|
196
|
+
}
|
|
197
|
+
/** Test-only: clear the in-memory cache between cases. */
|
|
198
|
+
export function _resetOrgDirectoryCache() {
|
|
199
|
+
cache.clear();
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=org-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-directory.js","sourceRoot":"","sources":["../../src/mcp/org-directory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Org-directory discovery for the generic cross-app MCP verbs\n * (`list_apps` / `ask_app` in `builtin-tools.ts`).\n *\n * Phase 3b of cross-app auto-wiring. Today the cross-app verbs resolve sibling\n * apps from *local workspace* info only (`workspace-resolve.ts`), so the mail\n * agent can only reach the calendar agent in a local dev workspace. When the\n * deployment runs against an org directory (Dispatch is also the identity hub\n * for the org), this module discovers the org's *deployed* sibling apps so the\n * same verbs work cross-app in production with ZERO manual setup.\n *\n * ## The directory request\n *\n * GET <directoryOrigin>/_agent-native/org/apps\n * Auth Authorization: Bearer <org A2A token> (same signed token A2A peers\n * already mint — reuses `resolveA2ACallerAuth()`; the org A2A secret /\n * global `A2A_SECRET` is loaded exactly how outgoing A2A calls load it)\n * ⇒ { org, apps: [ { id, name, url, a2aUrl, capabilities? } ] }\n * (allow-listed first-party apps only, prod URLs — enforced by the\n * authority side, Phase 3a, on Dispatch)\n *\n * ## Resolution + safety model\n *\n * - The directory origin is read from env: `AGENT_NATIVE_ORG_DIRECTORY_URL`\n * (dedicated) or `AGENT_NATIVE_IDENTITY_HUB_URL` (Dispatch is also the\n * identity hub). When *neither* is set the feature is simply inactive —\n * `fetchOrgApps()` returns `[]` and nothing changes anywhere (asserted by\n * a test). This makes the whole feature opt-in and back-compat.\n * - On ANY error (no env, unreachable, 401, non-2xx, bad JSON, no signed\n * token) `fetchOrgApps()` returns `[]` and NEVER throws — the cross-app\n * verbs degrade silently to their exact current local-only behavior.\n * - A short in-memory TTL cache (default 60s) keyed by directory origin and\n * caller identity/org scope so sibling app lists never cross tenants.\n * Empty authenticated results are cached too (with a shorter TTL) so a\n * transient failure doesn't hammer the directory on every call.\n * - No secrets are ever logged.\n *\n * Bundled alongside `mountMCP` (no Node-only top-level imports). The A2A\n * caller-auth + a2a client are dynamically imported inside `fetchOrgApps()`.\n */\n\nexport interface OrgApp {\n /** Canonical app id, e.g. `calendar`. */\n id: string;\n /** Human-readable name, e.g. `Calendar`. */\n name: string;\n /** Deployed app origin/URL, e.g. `https://calendar.acme.com`. */\n url: string;\n /**\n * A2A endpoint to route `ask_app` to. The authority side returns this; we\n * fall back to the app `url` (the A2A client appends `/_agent-native/a2a`).\n */\n a2aUrl: string;\n /** Optional capability hints the authority side may include. */\n capabilities?: string[];\n}\n\n/** Default cache TTL for a successful directory fetch. */\nconst SUCCESS_TTL_MS = 60_000;\n/** Shorter TTL for an empty/failed fetch so transient errors recover fast. */\nconst EMPTY_TTL_MS = 10_000;\n\ninterface CacheEntry {\n apps: OrgApp[];\n expiresAt: number;\n}\n\n/** In-memory cache keyed by resolved directory origin (+ identity scope). */\nconst cache = new Map<string, CacheEntry>();\n\n/**\n * Resolve the org-directory origin from env. Returns `null` when neither env\n * var is set — the caller treats `null` as \"feature inactive\".\n *\n * `env` is injectable for tests; defaults to `process.env`.\n */\nexport function resolveOrgDirectoryOrigin(\n env: NodeJS.ProcessEnv = process.env,\n): string | null {\n const raw =\n env.AGENT_NATIVE_ORG_DIRECTORY_URL || env.AGENT_NATIVE_IDENTITY_HUB_URL;\n if (!raw || typeof raw !== \"string\") return null;\n const trimmed = raw.trim().replace(/\\/+$/, \"\");\n if (!trimmed) return null;\n try {\n // Validate it's an absolute http(s) URL; reject anything else.\n const u = new URL(trimmed);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return null;\n return trimmed;\n } catch {\n return null;\n }\n}\n\nfunction normalizeApp(raw: unknown): OrgApp | null {\n if (!raw || typeof raw !== \"object\") return null;\n const r = raw as Record<string, unknown>;\n const id = typeof r.id === \"string\" ? r.id.trim().toLowerCase() : \"\";\n const url = typeof r.url === \"string\" ? r.url.trim() : \"\";\n if (!id || !url) return null;\n // Only accept absolute http(s) URLs from the directory.\n try {\n const u = new URL(url);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return null;\n } catch {\n return null;\n }\n const name = typeof r.name === \"string\" && r.name.trim() ? r.name.trim() : id;\n const a2aUrl =\n typeof r.a2aUrl === \"string\" && r.a2aUrl.trim() ? r.a2aUrl.trim() : url;\n const capabilities = Array.isArray(r.capabilities)\n ? r.capabilities.filter((c): c is string => typeof c === \"string\")\n : undefined;\n return {\n id,\n name,\n url: url.replace(/\\/+$/, \"\"),\n a2aUrl: a2aUrl.replace(/\\/+$/, \"\"),\n ...(capabilities && capabilities.length ? { capabilities } : {}),\n };\n}\n\n/** Compare two origins by host (ignores trailing slash / protocol noise). */\nfunction sameOrigin(a: string, b: string): boolean {\n try {\n const ua = new URL(a);\n const ub = new URL(b);\n return ua.host === ub.host && ua.protocol === ub.protocol;\n } catch {\n return a.replace(/\\/+$/, \"\") === b.replace(/\\/+$/, \"\");\n }\n}\n\nfunction scopedCacheKey(\n origin: string,\n auth: {\n userEmail?: string;\n orgId?: string;\n orgDomain?: string;\n },\n): string {\n return [\n origin,\n `user:${auth.userEmail ?? \"\"}`,\n `org:${auth.orgId ?? auth.orgDomain ?? \"\"}`,\n ].join(\"|\");\n}\n\n/**\n * Fetch the org's first-party sibling apps from the org directory.\n *\n * - Returns `[]` (never throws) on ANY failure or when the directory env is\n * unset — the cross-app verbs then keep their exact local-only behavior.\n * - Short in-memory TTL cache so it isn't fetched on every tool call.\n * - Strips the current app from the result (compared by id and by origin) so\n * `list_apps` / `ask_app` never offer to route to themselves.\n *\n * @param opts.selfId Current app id (so it's stripped from the result).\n * @param opts.selfOrigin Current app origin (so it's stripped by origin too).\n * @param opts.env Injectable env (tests). Defaults to `process.env`.\n */\nexport async function fetchOrgApps(opts?: {\n selfId?: string;\n selfOrigin?: string;\n env?: NodeJS.ProcessEnv;\n}): Promise<OrgApp[]> {\n const env = opts?.env ?? process.env;\n const origin = resolveOrgDirectoryOrigin(env);\n // Feature inactive: no directory configured ⇒ behave exactly as before.\n if (!origin) return [];\n\n const selfId = (opts?.selfId ?? \"\").trim().toLowerCase();\n const selfOrigin = (opts?.selfOrigin ?? \"\").trim();\n\n const stripSelf = (apps: OrgApp[]): OrgApp[] =>\n apps.filter((a) => {\n if (selfId && a.id === selfId) return false;\n if (selfOrigin && sameOrigin(a.url, selfOrigin)) return false;\n return true;\n });\n\n let cacheKey: string | null = null;\n let apps: OrgApp[] = [];\n let ttl = EMPTY_TTL_MS;\n try {\n // Reuse the existing A2A caller-auth: it reads userEmail + orgId from the\n // request context, loads the org A2A secret via getOrgA2ASecret (falling\n // back to the global A2A_SECRET env), and signs the same bearer JWT A2A\n // peers already use. No new secret loading is invented here.\n const { resolveA2ACallerAuth } = await import(\"../a2a/caller-auth.js\");\n const auth = await resolveA2ACallerAuth();\n if (!auth.apiKey) {\n // No signed token available (no A2A secret / no caller identity) — the\n // directory requires the org bearer, so degrade silently to local-only.\n return [];\n }\n\n const now = Date.now();\n cacheKey = scopedCacheKey(origin, auth);\n const cached = cache.get(cacheKey);\n if (cached && cached.expiresAt > now) {\n return stripSelf(cached.apps);\n }\n\n const res = await fetch(`${origin}/_agent-native/org/apps`, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${auth.apiKey}`,\n Accept: \"application/json\",\n },\n signal: AbortSignal.timeout(4000),\n });\n if (res.ok) {\n const json = (await res.json()) as { apps?: unknown };\n const list = Array.isArray(json?.apps) ? json.apps : [];\n apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);\n ttl = SUCCESS_TTL_MS;\n }\n // Non-2xx ⇒ leave apps=[] with the short EMPTY_TTL (silent degrade).\n } catch {\n // Unreachable / parse error / abort ⇒ silent degrade to local-only.\n apps = [];\n ttl = EMPTY_TTL_MS;\n }\n\n if (cacheKey) {\n cache.set(cacheKey, {\n apps,\n expiresAt: Date.now() + ttl,\n });\n }\n return stripSelf(apps);\n}\n\n/** Test-only: clear the in-memory cache between cases. */\nexport function _resetOrgDirectoryCache(): void {\n cache.clear();\n}\n"]}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
1
2
|
import { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, type MCPConfig, type MCPCallerIdentity, type MCPRequestMeta } from "./build-server.js";
|
|
2
3
|
export { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, };
|
|
3
4
|
export type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };
|
|
5
|
+
/**
|
|
6
|
+
* Handle a single `{routePrefix}/mcp` request on either runtime.
|
|
7
|
+
*
|
|
8
|
+
* - **Node fast-path** (real Node HTTP server): unchanged — delegate to the
|
|
9
|
+
* SDK's `StreamableHTTPServerTransport.handleRequest(nodeReq, nodeRes,
|
|
10
|
+
* body)`, which writes directly to the Node response (full protocol incl.
|
|
11
|
+
* SSE).
|
|
12
|
+
* - **Web-standard fallback** (Nitro 3 / Netlify web runtime, Cloudflare,
|
|
13
|
+
* Deno, Bun — where there is no Node req/res): build the SAME MCP `Server`
|
|
14
|
+
* from the SAME config + identity, drive it through the SDK's
|
|
15
|
+
* `WebStandardStreamableHTTPServerTransport` (which the Node transport is
|
|
16
|
+
* itself just a thin wrapper around), and return the resulting Web
|
|
17
|
+
* `Response` as a normal h3 return value.
|
|
18
|
+
*
|
|
19
|
+
* Auth, the `runWithRequestContext` identity wrap, the deep-link `_meta` /
|
|
20
|
+
* markdown append, `requestMeta` origin/target derivation and the stateless
|
|
21
|
+
* semantics are IDENTICAL on both paths because both build the same server
|
|
22
|
+
* via `createMCPServerForRequest` and both transports funnel into the same
|
|
23
|
+
* `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {
|
|
24
|
+
* parsedBody })` with the same options.
|
|
25
|
+
*
|
|
26
|
+
* Returns:
|
|
27
|
+
* - `undefined` when the request targets a sub-route (so management/status
|
|
28
|
+
* routes mounted under `/_agent-native/mcp/*` handle it themselves) — the
|
|
29
|
+
* h3 mount falls through to the next handler.
|
|
30
|
+
* - a Web `Response` (web fallback) or a string/object (Node path /
|
|
31
|
+
* auth-error path) otherwise. The Node path also sets `_handled` so h3
|
|
32
|
+
* doesn't double-write.
|
|
33
|
+
*/
|
|
34
|
+
export declare function handleMcpRequest(event: H3Event, config: MCPConfig): Promise<Response | string | {
|
|
35
|
+
error: string;
|
|
36
|
+
} | undefined>;
|
|
4
37
|
/**
|
|
5
38
|
* Mount an MCP remote server on an H3/Nitro app.
|
|
6
39
|
*
|
|
7
40
|
* Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)
|
|
8
41
|
*
|
|
9
42
|
* Uses stateless Streamable HTTP transport — no in-memory sessions,
|
|
10
|
-
* compatible with serverless deployments.
|
|
43
|
+
* compatible with serverless deployments. Runtime-agnostic: a real Node
|
|
44
|
+
* server uses the SDK's Node transport; the web-standard runtime (Nitro 3 /
|
|
45
|
+
* Netlify web runtime, Cloudflare, Deno, Bun) uses the SDK's web-standard
|
|
46
|
+
* transport. Both build the same server and produce identical JSON-RPC
|
|
47
|
+
* output.
|
|
11
48
|
*
|
|
12
49
|
* Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.
|
|
13
50
|
* No auth required when neither is configured (dev mode).
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AASlC,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AA2G7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAgH5D;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,SAAS,EACjB,WAAW,SAAmB,GAC7B,IAAI,CAYN"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -7,98 +7,182 @@ import { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFro
|
|
|
7
7
|
// against `./server.js` exactly as before this refactor.
|
|
8
8
|
export { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, };
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
//
|
|
10
|
+
// Runtime detection — Node fast-path vs. web-standard fallback
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Resolve the underlying Node `http` req/res pair if (and only if) we're
|
|
14
|
+
* running on a real Node HTTP server (local dev, `node` Nitro preset). On the
|
|
15
|
+
* web-standard runtime (Nitro 3 / Netlify web runtime, Cloudflare, Deno, Bun)
|
|
16
|
+
* BOTH of these are undefined — that's the signal to take the web fallback
|
|
17
|
+
* instead of returning 501.
|
|
18
|
+
*/
|
|
19
|
+
function getNodeReqRes(event) {
|
|
20
|
+
const e = event;
|
|
21
|
+
const nodeReq = e.node?.req ?? e.req?.runtime?.node?.req;
|
|
22
|
+
const nodeRes = e.node?.res ?? e.req?.runtime?.node?.res;
|
|
23
|
+
return { nodeReq, nodeRes };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive the request origin + the markdown deep-link target from the inbound
|
|
27
|
+
* headers. Identical logic for both the Node and web paths so the absolute
|
|
28
|
+
* deep-link URLs in tool results are computed the same way regardless of
|
|
29
|
+
* runtime.
|
|
30
|
+
*/
|
|
31
|
+
function deriveRequestMeta(event) {
|
|
32
|
+
const forwardedProto = getRequestHeader(event, "x-forwarded-proto");
|
|
33
|
+
const host = getRequestHeader(event, "host");
|
|
34
|
+
const proto = forwardedProto?.split(",")[0]?.trim() ||
|
|
35
|
+
(host && /^(localhost|127\.0\.0\.1)(:|$)/.test(host) ? "http" : "https");
|
|
36
|
+
const origin = host ? `${proto}://${host}` : undefined;
|
|
37
|
+
const targetHeader = getRequestHeader(event, "x-agent-native-open-target")?.toLowerCase();
|
|
38
|
+
const target = targetHeader === "desktop" ||
|
|
39
|
+
targetHeader === "terminal" ||
|
|
40
|
+
targetHeader === "browser"
|
|
41
|
+
? targetHeader
|
|
42
|
+
: undefined;
|
|
43
|
+
return { origin, target };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reconstruct a Web Standard `Request` for the web-standard MCP transport.
|
|
14
47
|
*
|
|
15
|
-
*
|
|
48
|
+
* On the web runtime h3 v2 exposes the real web `Request` as `event.req`; we
|
|
49
|
+
* prefer it (its `method` / `headers` are exactly what the client sent). But
|
|
50
|
+
* the framework middleware rewrites `event.req.url` when it strips a mount
|
|
51
|
+
* prefix, and the transport reads `req.method` + `req.headers` (never the
|
|
52
|
+
* body — we pass that via `parsedBody`), so we always synthesize a clean
|
|
53
|
+
* `Request` with the verified method + a fresh `Headers` copy. The URL is
|
|
54
|
+
* cosmetic for the SDK (it only does `new URL(req.url)` for `requestInfo`),
|
|
55
|
+
* so a best-effort absolute URL derived from the inbound host is sufficient
|
|
56
|
+
* and never throws.
|
|
57
|
+
*/
|
|
58
|
+
function buildWebRequest(event, method) {
|
|
59
|
+
const src = event.req;
|
|
60
|
+
const headers = new Headers();
|
|
61
|
+
if (src?.headers && typeof src.headers.forEach === "function") {
|
|
62
|
+
src.headers.forEach((value, key) => headers.set(key, value));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const rawHeaders = event.node?.req?.headers;
|
|
66
|
+
if (rawHeaders) {
|
|
67
|
+
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
68
|
+
if (value == null)
|
|
69
|
+
continue;
|
|
70
|
+
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// The SDK requires Accept + Content-Type to advertise both JSON and SSE on
|
|
75
|
+
// a POST. Real MCP clients (Claude Code, `agent-native connect`) always
|
|
76
|
+
// send these; we never inject/alter them — if they're absent the SDK
|
|
77
|
+
// returns its spec-mandated 406/415, identical to the Node path.
|
|
78
|
+
const host = headers.get("host") || "localhost";
|
|
79
|
+
const forwardedProto = headers.get("x-forwarded-proto");
|
|
80
|
+
const proto = forwardedProto?.split(",")[0]?.trim() ||
|
|
81
|
+
(/^(localhost|127\.0\.0\.1)(:|$)/.test(host) ? "http" : "https");
|
|
82
|
+
let url = `${proto}://${host}/_agent-native/mcp`;
|
|
83
|
+
try {
|
|
84
|
+
if (src?.url)
|
|
85
|
+
url = new URL(src.url).href;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// keep the synthesized URL
|
|
89
|
+
}
|
|
90
|
+
// No body here on purpose: the JSON-RPC payload is forwarded via the
|
|
91
|
+
// transport's `parsedBody` option (the same mechanism the Node transport
|
|
92
|
+
// uses), so the request stream is never read twice.
|
|
93
|
+
return new Request(url, { method, headers });
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// handleMcpRequest — runtime-agnostic MCP request handler
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/**
|
|
99
|
+
* Handle a single `{routePrefix}/mcp` request on either runtime.
|
|
16
100
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
101
|
+
* - **Node fast-path** (real Node HTTP server): unchanged — delegate to the
|
|
102
|
+
* SDK's `StreamableHTTPServerTransport.handleRequest(nodeReq, nodeRes,
|
|
103
|
+
* body)`, which writes directly to the Node response (full protocol incl.
|
|
104
|
+
* SSE).
|
|
105
|
+
* - **Web-standard fallback** (Nitro 3 / Netlify web runtime, Cloudflare,
|
|
106
|
+
* Deno, Bun — where there is no Node req/res): build the SAME MCP `Server`
|
|
107
|
+
* from the SAME config + identity, drive it through the SDK's
|
|
108
|
+
* `WebStandardStreamableHTTPServerTransport` (which the Node transport is
|
|
109
|
+
* itself just a thin wrapper around), and return the resulting Web
|
|
110
|
+
* `Response` as a normal h3 return value.
|
|
19
111
|
*
|
|
20
|
-
* Auth
|
|
21
|
-
*
|
|
112
|
+
* Auth, the `runWithRequestContext` identity wrap, the deep-link `_meta` /
|
|
113
|
+
* markdown append, `requestMeta` origin/target derivation and the stateless
|
|
114
|
+
* semantics are IDENTICAL on both paths because both build the same server
|
|
115
|
+
* via `createMCPServerForRequest` and both transports funnel into the same
|
|
116
|
+
* `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {
|
|
117
|
+
* parsedBody })` with the same options.
|
|
118
|
+
*
|
|
119
|
+
* Returns:
|
|
120
|
+
* - `undefined` when the request targets a sub-route (so management/status
|
|
121
|
+
* routes mounted under `/_agent-native/mcp/*` handle it themselves) — the
|
|
122
|
+
* h3 mount falls through to the next handler.
|
|
123
|
+
* - a Web `Response` (web fallback) or a string/object (Node path /
|
|
124
|
+
* auth-error path) otherwise. The Node path also sets `_handled` so h3
|
|
125
|
+
* doesn't double-write.
|
|
22
126
|
*/
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
127
|
+
export async function handleMcpRequest(event, config) {
|
|
128
|
+
const pathname = event.url?.pathname || "/";
|
|
129
|
+
const subpath = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
130
|
+
if (subpath) {
|
|
131
|
+
// Let management/status routes mounted under /_agent-native/mcp/* handle
|
|
132
|
+
// their own requests instead of treating them as MCP protocol traffic.
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const method = getMethod(event);
|
|
136
|
+
// Auth check — extracts the caller's identity from the JWT (`sub`), or, on
|
|
137
|
+
// the static-token / dev-open path, from the forwarded
|
|
138
|
+
// `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the
|
|
139
|
+
// `agent-native mcp install` flow). Without this the install flow would run
|
|
140
|
+
// every tool unscoped (userEmail === undefined).
|
|
141
|
+
const authHeader = getRequestHeader(event, "authorization");
|
|
142
|
+
const ownerEmailHeader = getRequestHeader(event, "x-agent-native-owner-email");
|
|
143
|
+
const authResult = await verifyAuth(authHeader, ownerEmailHeader);
|
|
144
|
+
if (!authResult.authed) {
|
|
145
|
+
setResponseStatus(event, 401);
|
|
146
|
+
return { error: "Unauthorized" };
|
|
147
|
+
}
|
|
148
|
+
// Stateless mode: only POST is meaningful
|
|
149
|
+
if (method === "DELETE") {
|
|
150
|
+
setResponseStatus(event, 204);
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
if (method !== "POST" && method !== "GET") {
|
|
154
|
+
setResponseStatus(event, 405);
|
|
155
|
+
return { error: "Method not allowed" };
|
|
156
|
+
}
|
|
157
|
+
// Read body for POST (GET has no body). Read it via the h3 helper exactly
|
|
158
|
+
// once; both transports accept it as a pre-parsed body so the request
|
|
159
|
+
// stream is never consumed twice.
|
|
160
|
+
const body = method === "POST" ? await readBody(event) : undefined;
|
|
161
|
+
// Per-request stateless transport + server. Both runtimes build the SAME
|
|
162
|
+
// server from the SAME config + verified identity + request meta, so
|
|
163
|
+
// tools/list, tools/call, and the deep-link `_meta` are identical.
|
|
164
|
+
const requestMeta = deriveRequestMeta(event);
|
|
165
|
+
const server = await createMCPServerForRequest(config, authResult.identity, requestMeta);
|
|
166
|
+
const { nodeReq, nodeRes } = getNodeReqRes(event);
|
|
167
|
+
if (nodeReq && nodeRes) {
|
|
168
|
+
// ---- Node fast-path (UNCHANGED behavior) --------------------------------
|
|
62
169
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
63
170
|
const transport = new StreamableHTTPServerTransport({
|
|
64
171
|
sessionIdGenerator: undefined, // stateless
|
|
65
172
|
});
|
|
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 });
|
|
82
173
|
await server.connect(transport);
|
|
83
|
-
// Delegate to the transport — it writes directly to the Node response.
|
|
84
|
-
// MCP's HTTP transport requires Node streams; this route is Node-only.
|
|
85
|
-
const nodeReq = event.node?.req ?? event.req?.runtime?.node?.req;
|
|
86
|
-
const nodeRes = event.node?.res ?? event.req?.runtime?.node?.res;
|
|
87
|
-
if (!nodeReq || !nodeRes) {
|
|
88
|
-
setResponseStatus(event, 501);
|
|
89
|
-
return { error: "MCP requires Node runtime" };
|
|
90
|
-
}
|
|
91
174
|
try {
|
|
175
|
+
// The SDK transport writes directly to the Node response. Node-only by
|
|
176
|
+
// construction; we only reach here when real Node req/res exist.
|
|
92
177
|
await transport.handleRequest(nodeReq, nodeRes, body);
|
|
93
178
|
}
|
|
94
179
|
catch (err) {
|
|
95
|
-
// The SDK transport writes directly to the Node response. If the
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// anything else.
|
|
180
|
+
// The SDK transport writes directly to the Node response. If the socket
|
|
181
|
+
// is already closed/ended (client disconnected, or the host stream
|
|
182
|
+
// layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END *after*
|
|
183
|
+
// the MCP payload was already delivered correctly. Swallow that benign
|
|
184
|
+
// post-flush write so an external agent disconnecting mid-stream can
|
|
185
|
+
// never take down the server process; rethrow anything else.
|
|
102
186
|
if (err?.code !== "ERR_STREAM_WRITE_AFTER_END")
|
|
103
187
|
throw err;
|
|
104
188
|
if (process.env.DEBUG)
|
|
@@ -106,6 +190,53 @@ export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
|
106
190
|
}
|
|
107
191
|
// Prevent H3 from double-writing the response
|
|
108
192
|
event._handled = true;
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
// ---- Web-standard fallback (Nitro 3 / Netlify web runtime, CF, Deno,
|
|
196
|
+
// Bun) ---------------------------------------------------------------------
|
|
197
|
+
//
|
|
198
|
+
// `StreamableHTTPServerTransport` is itself just a thin wrapper that
|
|
199
|
+
// converts the Node req/res to a web Request/Response and delegates to
|
|
200
|
+
// `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {
|
|
201
|
+
// parsedBody })`. Using the web transport directly with the SAME options +
|
|
202
|
+
// the same pre-read `parsedBody` produces byte-identical protocol output
|
|
203
|
+
// (including the deep-link `_meta` built inside createMCPServerForRequest),
|
|
204
|
+
// and works on every web runtime because it returns a Web `Response`
|
|
205
|
+
// (JSON for request/response, or an SSE `ReadableStream` body which h3
|
|
206
|
+
// streams natively).
|
|
207
|
+
const { WebStandardStreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
208
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
209
|
+
sessionIdGenerator: undefined, // stateless — same as the Node path
|
|
210
|
+
});
|
|
211
|
+
await server.connect(transport);
|
|
212
|
+
const webRequest = buildWebRequest(event, method);
|
|
213
|
+
// `parsedBody: undefined` would make the SDK try to read `req.json()`; our
|
|
214
|
+
// synthesized request has no body, so only pass the option for POST (where
|
|
215
|
+
// we actually have a parsed body). For GET the transport reads no body.
|
|
216
|
+
const response = await transport.handleRequest(webRequest, method === "POST" ? { parsedBody: body } : undefined);
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
/**
|
|
223
|
+
* Mount an MCP remote server on an H3/Nitro app.
|
|
224
|
+
*
|
|
225
|
+
* Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)
|
|
226
|
+
*
|
|
227
|
+
* Uses stateless Streamable HTTP transport — no in-memory sessions,
|
|
228
|
+
* compatible with serverless deployments. Runtime-agnostic: a real Node
|
|
229
|
+
* server uses the SDK's Node transport; the web-standard runtime (Nitro 3 /
|
|
230
|
+
* Netlify web runtime, Cloudflare, Deno, Bun) uses the SDK's web-standard
|
|
231
|
+
* transport. Both build the same server and produce identical JSON-RPC
|
|
232
|
+
* output.
|
|
233
|
+
*
|
|
234
|
+
* Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.
|
|
235
|
+
* No auth required when neither is configured (dev mode).
|
|
236
|
+
*/
|
|
237
|
+
export function mountMCP(nitroApp, config, routePrefix = "/_agent-native") {
|
|
238
|
+
getH3App(nitroApp).use(`${routePrefix}/mcp`, defineEventHandler(async (event) => {
|
|
239
|
+
return handleMcpRequest(event, config);
|
|
109
240
|
}));
|
|
110
241
|
if (process.env.DEBUG)
|
|
111
242
|
console.log(`[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? " + ask-agent" : ""})`);
|