@fastagent-sh/fastagent 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +10 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +3 -1
  15. package/dist/channels/feishu/feishu-api.js +4 -0
  16. package/dist/channels/feishu/feishu.d.ts +0 -3
  17. package/dist/channels/feishu/feishu.js +10 -10
  18. package/dist/channels/feishu/register-app.d.ts +2 -0
  19. package/dist/channels/feishu/register-app.js +4 -6
  20. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  21. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  22. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  23. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  24. package/dist/channels/feishu/setup-mode.js +2 -22
  25. package/dist/channels/feishu/setup.d.ts +12 -0
  26. package/dist/channels/feishu/setup.js +197 -0
  27. package/dist/channels/kit/abortable.d.ts +8 -0
  28. package/dist/channels/kit/abortable.js +22 -0
  29. package/dist/channels/lark/lark.d.ts +0 -3
  30. package/dist/channels/lark/lark.js +0 -5
  31. package/dist/channels/lark/scaffold/channel.ts +33 -35
  32. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  33. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  34. package/dist/channels/lark/setup.d.ts +9 -0
  35. package/dist/channels/lark/setup.js +57 -0
  36. package/dist/channels/setup.d.ts +112 -0
  37. package/dist/channels/setup.js +31 -0
  38. package/dist/channels/slack/history.js +1 -1
  39. package/dist/channels/slack/manifest.d.ts +17 -7
  40. package/dist/channels/slack/manifest.js +12 -16
  41. package/dist/channels/slack/scaffold/channel.ts +44 -30
  42. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  43. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  44. package/dist/channels/slack/setup.d.ts +7 -0
  45. package/dist/channels/slack/setup.js +119 -0
  46. package/dist/channels/slack/slack-api.d.ts +2 -0
  47. package/dist/channels/slack/slack-api.js +4 -0
  48. package/dist/channels/slack/slack.d.ts +21 -2
  49. package/dist/channels/slack/slack.js +398 -338
  50. package/dist/channels/slack/socket.d.ts +57 -0
  51. package/dist/channels/slack/socket.js +305 -0
  52. package/dist/channels/telegram/polling.d.ts +27 -0
  53. package/dist/channels/telegram/polling.js +105 -0
  54. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  55. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  56. package/dist/channels/telegram/setup.d.ts +2 -0
  57. package/dist/channels/telegram/setup.js +42 -0
  58. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  59. package/dist/channels/telegram/telegram-api.js +7 -6
  60. package/dist/channels/telegram/telegram.d.ts +11 -3
  61. package/dist/channels/telegram/telegram.js +184 -157
  62. package/dist/channels/wait-health.js +1 -1
  63. package/dist/channels/webhook-evidence.d.ts +11 -0
  64. package/dist/channels/webhook-evidence.js +71 -0
  65. package/dist/channels/webhooks.d.ts +37 -0
  66. package/dist/channels/webhooks.js +70 -0
  67. package/dist/child-env.d.ts +16 -0
  68. package/dist/child-env.js +30 -0
  69. package/dist/cli/box-login.js +4 -1
  70. package/dist/cli/channel-app.d.ts +3 -0
  71. package/dist/cli/channel-app.js +35 -0
  72. package/dist/cli/channel-setup.d.ts +38 -0
  73. package/dist/cli/channel-setup.js +195 -0
  74. package/dist/cli/commands/add.d.ts +6 -14
  75. package/dist/cli/commands/add.js +45 -310
  76. package/dist/cli/commands/auth.js +8 -2
  77. package/dist/cli/commands/build.d.ts +5 -0
  78. package/dist/cli/commands/build.js +16 -0
  79. package/dist/cli/commands/channels.d.ts +19 -0
  80. package/dist/cli/commands/channels.js +230 -0
  81. package/dist/cli/commands/content.d.ts +7 -3
  82. package/dist/cli/commands/content.js +16 -10
  83. package/dist/cli/commands/deploy/agentcore.js +55 -23
  84. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  85. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  86. package/dist/cli/commands/deploy/docker.js +32 -65
  87. package/dist/cli/commands/deploy/fly.js +28 -12
  88. package/dist/cli/commands/deploy/railway.js +38 -13
  89. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  90. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  91. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  92. package/dist/cli/commands/deploy/shared.js +25 -8
  93. package/dist/cli/commands/deploy.js +53 -67
  94. package/dist/cli/commands/dev.d.ts +2 -0
  95. package/dist/cli/commands/dev.js +15 -3
  96. package/dist/cli/commands/env.js +1 -1
  97. package/dist/cli/commands/info.d.ts +2 -0
  98. package/dist/cli/commands/info.js +63 -24
  99. package/dist/cli/commands/init.js +3 -1
  100. package/dist/cli/commands/invoke.d.ts +2 -0
  101. package/dist/cli/commands/invoke.js +20 -7
  102. package/dist/cli/commands/login.d.ts +0 -7
  103. package/dist/cli/commands/login.js +22 -10
  104. package/dist/cli/commands/logout.js +1 -1
  105. package/dist/cli/commands/mcp.d.ts +12 -0
  106. package/dist/cli/commands/mcp.js +164 -0
  107. package/dist/cli/commands/models.js +3 -2
  108. package/dist/cli/commands/schedules.d.ts +4 -2
  109. package/dist/cli/commands/schedules.js +14 -10
  110. package/dist/cli/commands/secrets.d.ts +14 -0
  111. package/dist/cli/commands/secrets.js +213 -0
  112. package/dist/cli/commands/start.d.ts +14 -14
  113. package/dist/cli/commands/start.js +46 -60
  114. package/dist/cli/content-view.d.ts +4 -6
  115. package/dist/cli/content-view.js +5 -11
  116. package/dist/cli/first-run-channels.d.ts +3 -0
  117. package/dist/cli/first-run-channels.js +63 -0
  118. package/dist/cli/instance.d.ts +41 -0
  119. package/dist/cli/instance.js +155 -0
  120. package/dist/cli/invoke-stream.d.ts +5 -2
  121. package/dist/cli/invoke-stream.js +7 -2
  122. package/dist/cli/program.js +339 -108
  123. package/dist/cli/serve.d.ts +4 -5
  124. package/dist/cli/serve.js +4 -7
  125. package/dist/cli/shared.d.ts +11 -2
  126. package/dist/cli/shared.js +32 -19
  127. package/dist/cli/ticket.js +50 -6
  128. package/dist/content/declare.d.ts +31 -37
  129. package/dist/content/declare.js +59 -60
  130. package/dist/content/file.d.ts +23 -12
  131. package/dist/content/file.js +48 -35
  132. package/dist/content/git.d.ts +0 -10
  133. package/dist/content/git.js +5 -39
  134. package/dist/content/resolve.d.ts +18 -22
  135. package/dist/content/resolve.js +32 -66
  136. package/dist/content/source.d.ts +7 -8
  137. package/dist/content/source.js +10 -28
  138. package/dist/credentials-page.d.ts +26 -0
  139. package/dist/credentials-page.js +185 -0
  140. package/dist/declared-secrets.d.ts +10 -3
  141. package/dist/declared-secrets.js +8 -4
  142. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  143. package/dist/deploy/agentcore/destroy.js +26 -2
  144. package/dist/deploy/agentcore/forwarder.js +1 -1
  145. package/dist/deploy/agentcore/plan.d.ts +7 -21
  146. package/dist/deploy/agentcore/plan.js +47 -49
  147. package/dist/deploy/agentcore/run.d.ts +18 -10
  148. package/dist/deploy/agentcore/run.js +93 -70
  149. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  150. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  151. package/dist/deploy/agentcore/shell.js +12 -19
  152. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  153. package/dist/deploy/agentcore/sigv4.js +15 -0
  154. package/dist/deploy/agentcore/slr.d.ts +18 -0
  155. package/dist/deploy/agentcore/slr.js +27 -0
  156. package/dist/deploy/agentcore/store.d.ts +7 -0
  157. package/dist/deploy/agentcore/store.js +115 -0
  158. package/dist/deploy/api.d.ts +14 -14
  159. package/dist/deploy/api.js +26 -24
  160. package/dist/deploy/box-shell.d.ts +3 -1
  161. package/dist/deploy/build-context.d.ts +4 -4
  162. package/dist/deploy/build-context.js +11 -11
  163. package/dist/deploy/channel-ingress.d.ts +35 -9
  164. package/dist/deploy/channel-ingress.js +119 -22
  165. package/dist/deploy/container.d.ts +35 -10
  166. package/dist/deploy/container.js +80 -39
  167. package/dist/deploy/delivered-text.d.ts +13 -0
  168. package/dist/deploy/delivered-text.js +86 -0
  169. package/dist/deploy/docker/plan.d.ts +7 -8
  170. package/dist/deploy/docker/plan.js +31 -52
  171. package/dist/deploy/docker/run.d.ts +18 -43
  172. package/dist/deploy/docker/run.js +57 -79
  173. package/dist/deploy/file-store.d.ts +2 -0
  174. package/dist/deploy/file-store.js +17 -0
  175. package/dist/deploy/fly/plan.d.ts +3 -2
  176. package/dist/deploy/fly/plan.js +20 -9
  177. package/dist/deploy/fly/run.d.ts +18 -22
  178. package/dist/deploy/fly/run.js +25 -44
  179. package/dist/deploy/fly/store.d.ts +12 -0
  180. package/dist/deploy/fly/store.js +83 -0
  181. package/dist/deploy/hosts.d.ts +1 -2
  182. package/dist/deploy/hosts.js +1 -2
  183. package/dist/deploy/preflight.d.ts +18 -5
  184. package/dist/deploy/preflight.js +135 -63
  185. package/dist/deploy/railway/ignore.d.ts +8 -0
  186. package/dist/deploy/railway/ignore.js +57 -0
  187. package/dist/deploy/railway/plan.d.ts +1 -2
  188. package/dist/deploy/railway/plan.js +8 -6
  189. package/dist/deploy/railway/run.d.ts +21 -15
  190. package/dist/deploy/railway/run.js +32 -78
  191. package/dist/deploy/railway/store.d.ts +29 -0
  192. package/dist/deploy/railway/store.js +143 -0
  193. package/dist/deploy/runner.d.ts +5 -2
  194. package/dist/deploy/runner.js +4 -3
  195. package/dist/deploy/secret-store.d.ts +63 -0
  196. package/dist/deploy/secret-store.js +98 -0
  197. package/dist/deploy/secrets.d.ts +29 -46
  198. package/dist/deploy/secrets.js +49 -122
  199. package/dist/deploy/workspace.d.ts +6 -12
  200. package/dist/deploy/workspace.js +18 -94
  201. package/dist/dev-supervisor.js +6 -7
  202. package/dist/environment/declare.js +3 -2
  203. package/dist/environment/mise.d.ts +3 -1
  204. package/dist/environment/mise.js +4 -2
  205. package/dist/feishu.d.ts +1 -1
  206. package/dist/feishu.js +1 -1
  207. package/dist/harnesses/models/catalog.d.ts +1 -0
  208. package/dist/harnesses/models/catalog.js +1 -2
  209. package/dist/harnesses/models/environment.d.ts +7 -2
  210. package/dist/harnesses/models/environment.js +9 -5
  211. package/dist/harnesses/models/host.d.ts +0 -7
  212. package/dist/harnesses/models/host.js +10 -1
  213. package/dist/harnesses/models/login.d.ts +0 -8
  214. package/dist/harnesses/models/login.js +5 -4
  215. package/dist/harnesses/models/models.d.ts +7 -2
  216. package/dist/harnesses/models/models.js +28 -4
  217. package/dist/harnesses/pi-durable/agent.js +59 -38
  218. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  219. package/dist/harnesses/pi-durable/build.js +270 -0
  220. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  221. package/dist/harnesses/pi-durable/built.js +136 -0
  222. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  223. package/dist/harnesses/pi-durable/config.js +266 -0
  224. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  225. package/dist/harnesses/pi-durable/define.js +23 -2
  226. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  227. package/dist/harnesses/pi-durable/definition.js +143 -0
  228. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  229. package/dist/harnesses/pi-durable/events.js +179 -136
  230. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  231. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  232. package/dist/harnesses/pi-durable/mcp.d.ts +3 -0
  233. package/dist/harnesses/pi-durable/mcp.js +32 -0
  234. package/dist/harnesses/pi-durable/open.d.ts +36 -38
  235. package/dist/harnesses/pi-durable/open.js +97 -150
  236. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  237. package/dist/harnesses/pi-durable/reads.js +1 -3
  238. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  239. package/dist/harnesses/pi-durable/registry.js +29 -26
  240. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  241. package/dist/harnesses/pi-durable/release.js +10 -7
  242. package/dist/harnesses/pi-durable/serve.d.ts +95 -15
  243. package/dist/harnesses/pi-durable/serve.js +151 -59
  244. package/dist/harnesses/pi-durable/sessions.js +15 -12
  245. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  246. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  247. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  248. package/dist/harnesses/pi-durable/text.js +115 -59
  249. package/dist/harnesses/pi-durable/wake.js +8 -8
  250. package/dist/http/client.js +9 -3
  251. package/dist/http/server.d.ts +1 -1
  252. package/dist/http/server.js +1 -1
  253. package/dist/index.d.ts +11 -4
  254. package/dist/index.js +8 -1
  255. package/dist/instance-name.d.ts +7 -0
  256. package/dist/instance-name.js +26 -0
  257. package/dist/lark.d.ts +1 -1
  258. package/dist/lark.js +1 -1
  259. package/dist/locked-file.d.ts +2 -2
  260. package/dist/locked-file.js +8 -3
  261. package/dist/mcp/auth.d.ts +22 -0
  262. package/dist/mcp/auth.js +180 -0
  263. package/dist/mcp/client.d.ts +15 -0
  264. package/dist/mcp/client.js +179 -0
  265. package/dist/mcp/config.d.ts +86 -0
  266. package/dist/mcp/config.js +225 -0
  267. package/dist/mcp/grants.d.ts +4 -0
  268. package/dist/mcp/grants.js +132 -0
  269. package/dist/mcp/host.d.ts +19 -0
  270. package/dist/mcp/host.js +128 -0
  271. package/dist/mcp/network.d.ts +12 -0
  272. package/dist/mcp/network.js +150 -0
  273. package/dist/mcp/sign-in.d.ts +2 -0
  274. package/dist/mcp/sign-in.js +204 -0
  275. package/dist/mcp-api.d.ts +107 -0
  276. package/dist/mcp-api.js +10 -0
  277. package/dist/paths.d.ts +22 -8
  278. package/dist/paths.js +44 -11
  279. package/dist/runtime-websocket.d.ts +14 -0
  280. package/dist/runtime-websocket.js +39 -0
  281. package/dist/scaffold/add-channel.d.ts +9 -16
  282. package/dist/scaffold/add-channel.js +13 -83
  283. package/dist/scaffold/templates/env.example +4 -5
  284. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  285. package/dist/scaffold/templates/gitignore +9 -6
  286. package/dist/scaffold/templates.d.ts +6 -2
  287. package/dist/scaffold/templates.js +7 -2
  288. package/dist/scaffold/vendor-skill.js +4 -2
  289. package/dist/service.d.ts +44 -6
  290. package/dist/service.js +110 -10
  291. package/dist/serving-command.d.ts +10 -0
  292. package/dist/serving-command.js +1 -0
  293. package/dist/setup-page.d.ts +51 -0
  294. package/dist/setup-page.js +149 -0
  295. package/dist/slack.d.ts +1 -1
  296. package/dist/slack.js +1 -1
  297. package/dist/strip-edges.d.ts +10 -0
  298. package/dist/strip-edges.js +22 -0
  299. package/dist/telegram.d.ts +1 -1
  300. package/dist/telegram.js +1 -1
  301. package/dist/tunnel.d.ts +0 -1
  302. package/dist/tunnel.js +2 -8
  303. package/dist/value-file.d.ts +12 -6
  304. package/dist/value-file.js +36 -25
  305. package/dist/version.d.ts +7 -1
  306. package/dist/version.js +9 -3
  307. package/package.json +8 -6
  308. package/dist/channels/lark/onboard.d.ts +0 -36
  309. package/dist/channels/lark/onboard.js +0 -56
  310. package/dist/channels/slack/config-api.d.ts +0 -69
  311. package/dist/channels/slack/config-api.js +0 -162
  312. package/dist/channels/slack/onboard.d.ts +0 -40
  313. package/dist/channels/slack/onboard.js +0 -136
  314. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  315. package/dist/channels/slack/onboarding-state.js +0 -64
  316. package/dist/channels/slack/register-webhook.d.ts +0 -12
  317. package/dist/channels/slack/register-webhook.js +0 -67
  318. package/dist/channels/slack/setup-server.d.ts +0 -32
  319. package/dist/channels/slack/setup-server.js +0 -201
  320. package/dist/cli/add-feishu.d.ts +0 -50
  321. package/dist/cli/add-feishu.js +0 -367
  322. package/dist/cli/add-slack.d.ts +0 -18
  323. package/dist/cli/add-slack.js +0 -216
  324. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  325. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The MCP servers an instance opens (agent-artifact.md §3.3, §4.2): the program's, the `mcpServers` map of its
3
+ * `context.json`, and what the instance adds, the `mcpServers` map of its `fastagent.json`. One list, read once, that
4
+ * every reader (the open, `inspectMcp`, sign-in, the secrets list, the build's warning) is given.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, resolve } from "node:path";
8
+ import * as Schema from "effect/Schema";
9
+ import * as SchemaIssue from "effect/SchemaIssue";
10
+ import { readProgramMcpServers } from "../content/file.js";
11
+ import { isEnvName } from "../declared-secrets.js";
12
+ import { McpError } from "../mcp-api.js";
13
+ import { CONTEXT_FILE } from "../paths.js";
14
+ const Strings = Schema.Record(Schema.String, Schema.String);
15
+ const Stdio = Schema.Struct({
16
+ type: Schema.optional(Schema.Literal("stdio")),
17
+ command: Schema.NonEmptyString,
18
+ args: Schema.optional(Schema.Array(Schema.String)),
19
+ env: Schema.optional(Strings),
20
+ });
21
+ const Http = Schema.Struct({
22
+ type: Schema.Literal("http"),
23
+ url: Schema.String,
24
+ headers: Schema.optional(Strings),
25
+ });
26
+ const Declaration = Schema.Union([Stdio, Http]);
27
+ const Servers = Schema.Record(Schema.String, Declaration);
28
+ /** The `mcpServers` map `raw` holds (undefined is none), checked; every refusal names `file` and, where it can, the server. */
29
+ function declareServers(raw, file) {
30
+ if (raw === undefined)
31
+ return {};
32
+ let servers;
33
+ try {
34
+ servers = Schema.decodeUnknownSync(Servers)(raw, { onExcessProperty: "error" });
35
+ }
36
+ catch (error) {
37
+ const paths = error instanceof Error && SchemaIssue.isIssue(error.cause)
38
+ ? SchemaIssue.makeFormatterStandardSchemaV1()(error.cause).issues.map((issue) => issue.path)
39
+ : [];
40
+ throw new McpError("configuration", `${file}: invalid MCP configuration at ${JSON.stringify(paths)}; expected stdio command/args/env or http url/headers; unknown fields are refused`);
41
+ }
42
+ for (const [name, server] of Object.entries(servers)) {
43
+ if (!/^[a-zA-Z0-9_-]+$/.test(name) || name.includes("__") || name.length > 40) {
44
+ throw new McpError("configuration", `${file}: server names must be 1–40 letters, digits, hyphens or underscores, without __`);
45
+ }
46
+ if (server.type === "http")
47
+ checkMcpUrl(server.url, name, file);
48
+ else
49
+ for (const key of Object.keys(server.env ?? {})) {
50
+ if (!isEnvName(key))
51
+ throw new McpError("configuration", `${file}: invalid environment variable name for ${name}`, name);
52
+ }
53
+ for (const value of configStrings(server))
54
+ variables(value, name, file);
55
+ }
56
+ return servers;
57
+ }
58
+ /** An instance's servers, checked: what `resolveConfig` asks of a configuration, so its refusal is the file's. */
59
+ export function checkInstanceServers(instance) {
60
+ declareServers(mapOf(instance.values), instance.label);
61
+ }
62
+ const mapOf = (values) => typeof values === "object" && values !== null ? values.mcpServers : undefined;
63
+ /**
64
+ * The servers of the program in `dir` and of `instance`. An instance only adds: a name both declare is refused, naming
65
+ * both, so nothing is shadowed.
66
+ */
67
+ export function declareMcp(dir, instance) {
68
+ const own = readProgramMcpServers(dir);
69
+ const program = Object.entries(declareServers(own.values, own.path)).map(([name, server]) => ({ name, server, file: CONTEXT_FILE, base: resolve(dir), origin: "program" }));
70
+ if (instance === undefined)
71
+ return program;
72
+ const added = Object.entries(declareServers(mapOf(instance.values), instance.label)).map(([name, server]) => ({
73
+ name,
74
+ server,
75
+ file: instance.label,
76
+ ...(instance.directory === undefined ? {} : { base: instance.directory }),
77
+ origin: "instance",
78
+ }));
79
+ for (const { name } of added) {
80
+ if (program.some((declared) => declared.name === name)) {
81
+ throw new McpError("configuration", `MCP server "${name}" is declared by the program (${CONTEXT_FILE}) and by the instance (${instance.label}): an ` +
82
+ `instance adds to the program's servers and does not replace them — name it otherwise`);
83
+ }
84
+ }
85
+ return [...program, ...added];
86
+ }
87
+ /** An instance configuration given to `inspectMcp` or `signInMcp`: an object, or the absolute path of a `fastagent.json`. */
88
+ export function mcpInstance(config) {
89
+ if (config === undefined)
90
+ return undefined;
91
+ if (typeof config !== "string")
92
+ return { values: config, label: "the config object" };
93
+ if (!isAbsolute(config))
94
+ throw new McpError("configuration", `config must be an absolute path, and is ${JSON.stringify(config)}`);
95
+ let text;
96
+ try {
97
+ text = readFileSync(config, "utf8");
98
+ }
99
+ catch (error) {
100
+ throw new McpError("configuration", `cannot read the instance configuration ${config}: ${error.message}`);
101
+ }
102
+ try {
103
+ return { values: JSON.parse(text), label: config, directory: dirname(config) };
104
+ }
105
+ catch (error) {
106
+ throw new McpError("configuration", `${config} is not JSON: ${error.message}`);
107
+ }
108
+ }
109
+ function configStrings(server) {
110
+ return server.type === "http"
111
+ ? Object.values(server.headers ?? {})
112
+ : [server.command, ...(server.args ?? []), ...Object.values(server.env ?? {})];
113
+ }
114
+ function variables(value, server, file) {
115
+ const names = [];
116
+ const rest = value.replace(/\$\{([^}]*)\}/g, (_match, name) => {
117
+ if (!isEnvName(name))
118
+ throw new McpError("configuration", `${file}: ${server} supports only \${NAME} substitutions`, server);
119
+ names.push(name);
120
+ return "";
121
+ });
122
+ if (rest.includes("${") || rest.includes("$("))
123
+ throw new McpError("configuration", `${file}: unsupported substitution in ${server}`, server);
124
+ return names;
125
+ }
126
+ /** The secrets the servers' `${NAME}` references need: each required, declared where the server is. */
127
+ export function mcpSecrets(declarations) {
128
+ return declarations.flatMap(({ name, server, file }) => configStrings(server).flatMap((value) => variables(value, name, file).map((secret) => ({
129
+ name: secret,
130
+ declaredIn: `${file} mcpServers.${name}`,
131
+ optional: false,
132
+ }))));
133
+ }
134
+ /**
135
+ * The header and environment values with no `${VAR}` in them: the format allows a literal credential, and where the
136
+ * file travels (a build publishes it, a deploy bakes or delivers it) each is said. FastAgent cannot tell a secret from a
137
+ * setting, so a caller warns and never refuses.
138
+ */
139
+ export function literalCredentials(declarations) {
140
+ return declarations.flatMap(({ name, server, file }) => {
141
+ const [field, entries] = server.type === "http" ? ["headers", server.headers ?? {}] : ["env", server.env ?? {}];
142
+ return Object.entries(entries)
143
+ .filter(([, value]) => value !== "" && !value.includes("${"))
144
+ .map(([key]) => ({ file, server: name, field, key }));
145
+ });
146
+ }
147
+ /**
148
+ * The stdio servers whose command is a path relative to the file that declares it, as it is written: what a build or a
149
+ * deploy has to be able to deliver, since a script is not found on a `PATH`. A command that starts with a `${VAR}` is
150
+ * left to the variable, and `args` are never read as paths, since FastAgent cannot know which strings are.
151
+ */
152
+ export function relativeCommands(declarations) {
153
+ return declarations.flatMap((declaration) => {
154
+ const { server } = declaration;
155
+ const command = server.type === "http" ? undefined : server.command;
156
+ return command !== undefined && /[/\\]/.test(command) && !isAbsolute(command) && !command.startsWith("${")
157
+ ? [{ ...declaration, command }]
158
+ : [];
159
+ });
160
+ }
161
+ /** What to write instead of a literal credential, which a header's value usually carries a scheme in front of. */
162
+ export function literalCredentialAdvice({ field, key }) {
163
+ const name = key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
164
+ return field === "headers"
165
+ ? `replace the secret in it with \${${name}} (a scheme stays: \`Bearer \${${name}}\`)`
166
+ : `write \${${name}}`;
167
+ }
168
+ export function checkMcpUrl(value, server, file = "mcpServers") {
169
+ const url = URL.parse(value);
170
+ if (!url ||
171
+ value.includes("${") ||
172
+ value.includes("$(") ||
173
+ url.username ||
174
+ url.password ||
175
+ url.hash ||
176
+ (url.protocol !== "https:" &&
177
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))) {
178
+ throw new McpError("configuration", `${file}: MCP endpoints require HTTPS (HTTP is allowed only on loopback), without userinfo or fragments`, server);
179
+ }
180
+ return url;
181
+ }
182
+ export function resolveMcpServers(declarations, env, only,
183
+ /** Where a stdio server runs: the instance's workspace, whoever declared it. */
184
+ cwd,
185
+ /** What every process the instance starts gets (child-env.ts), under a stdio server's own `env`. */
186
+ processEnv) {
187
+ return declarations
188
+ .filter(({ name }) => only === undefined || name === only)
189
+ .map(({ name, server, file, base }) => {
190
+ const expand = (value) => value.replace(/\$\{([^}]*)\}/g, (_match, key) => {
191
+ const result = env[key];
192
+ if (!result)
193
+ throw new McpError("configuration", `${file}: ${name} requires ${key}`, name);
194
+ return result;
195
+ });
196
+ if (server.type === "http") {
197
+ const headers = Object.fromEntries(Object.entries(server.headers ?? {}).map(([key, value]) => [key, expand(value)]));
198
+ try {
199
+ new Headers(headers);
200
+ }
201
+ catch {
202
+ throw new McpError("configuration", `${file}: invalid HTTP headers for ${name}`, name);
203
+ }
204
+ return { name, type: "http", url: checkMcpUrl(server.url, name, file).href, headers };
205
+ }
206
+ let command = expand(server.command);
207
+ if (!isAbsolute(command) && /[/\\]/.test(command)) {
208
+ if (base === undefined) {
209
+ throw new McpError("configuration", `${file}: the command "${command}" has a path and no file to be relative to: write an absolute path or a bare command`, name);
210
+ }
211
+ command = resolve(base, command);
212
+ }
213
+ return {
214
+ name,
215
+ type: "stdio",
216
+ command,
217
+ args: (server.args ?? []).map(expand),
218
+ cwd: resolve(cwd),
219
+ env: {
220
+ ...processEnv,
221
+ ...Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, expand(value)])),
222
+ },
223
+ };
224
+ });
225
+ }
@@ -0,0 +1,4 @@
1
+ import { type McpGrantRecord, type McpGrantStore, type McpGrantSummary } from "../mcp-api.ts";
2
+ export declare function checkedGrant(value: unknown): McpGrantRecord;
3
+ export declare function grantSummary(id: string, record: McpGrantRecord): McpGrantSummary;
4
+ export declare function mcpGrantFile(path: string): McpGrantStore;
@@ -0,0 +1,132 @@
1
+ /** Durable OAuth grants: explicit writes under the same lock that guards refresh and deletion. */
2
+ import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ import * as Schema from "effect/Schema";
5
+ import { withLockedFile } from "../locked-file.js";
6
+ import { McpError } from "../mcp-api.js";
7
+ const RecordSchema = Schema.Struct({
8
+ version: Schema.Literal(1),
9
+ serverUrl: Schema.NonEmptyString,
10
+ resource: Schema.NonEmptyString,
11
+ issuer: Schema.NonEmptyString,
12
+ client: Schema.Struct({
13
+ id: Schema.NonEmptyString,
14
+ secret: Schema.optional(Schema.String),
15
+ tokenEndpointAuthMethod: Schema.Literals(["none", "client_secret_post", "client_secret_basic"]),
16
+ }),
17
+ scopes: Schema.Array(Schema.NonEmptyString),
18
+ status: Schema.Literals(["ready", "refreshing", "reauthorization_required"]),
19
+ generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
20
+ accessToken: Schema.NonEmptyString,
21
+ refreshToken: Schema.optional(Schema.NonEmptyString),
22
+ expiresAt: Schema.optional(Schema.Number.check(Schema.isFinite())),
23
+ });
24
+ const FileSchema = Schema.Record(Schema.String, RecordSchema);
25
+ export function checkedGrant(value) {
26
+ try {
27
+ return Schema.decodeUnknownSync(RecordSchema)(value, { onExcessProperty: "error" });
28
+ }
29
+ catch {
30
+ throw new McpError("storage", "Invalid MCP grant record; refusing to read or overwrite credentials");
31
+ }
32
+ }
33
+ export function grantSummary(id, record) {
34
+ return {
35
+ id,
36
+ serverUrl: record.serverUrl,
37
+ resource: record.resource,
38
+ issuer: record.issuer,
39
+ scopes: [...record.scopes],
40
+ status: record.status,
41
+ ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
42
+ };
43
+ }
44
+ function decode(raw, path) {
45
+ if (raw === undefined)
46
+ return {};
47
+ try {
48
+ return Schema.decodeUnknownSync(Schema.fromJsonString(FileSchema))(raw, { onExcessProperty: "error" });
49
+ }
50
+ catch {
51
+ throw new McpError("storage", `Invalid MCP grant file ${path}; refusing to overwrite it`);
52
+ }
53
+ }
54
+ function idIsValid(id) {
55
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)) {
56
+ throw new McpError("storage", "Invalid MCP grant ID");
57
+ }
58
+ }
59
+ function grantFileTarget(path) {
60
+ try {
61
+ return realpathSync(path);
62
+ }
63
+ catch (error) {
64
+ if (error.code !== "ENOENT")
65
+ throw error;
66
+ const link = lstatSync(path, { throwIfNoEntry: false });
67
+ return link?.isSymbolicLink()
68
+ ? grantFileTarget(resolve(dirname(path), readlinkSync(path)))
69
+ : resolve(grantFileTarget(dirname(path)), basename(path));
70
+ }
71
+ }
72
+ export function mcpGrantFile(path) {
73
+ const file = resolve(path);
74
+ const read = () => {
75
+ try {
76
+ return decode(readFileSync(file, "utf8"), file);
77
+ }
78
+ catch (error) {
79
+ if (error.code === "ENOENT")
80
+ return {};
81
+ throw error;
82
+ }
83
+ };
84
+ return {
85
+ async read(id) {
86
+ idIsValid(id);
87
+ const records = read();
88
+ return Object.hasOwn(records, id) ? records[id] : undefined;
89
+ },
90
+ async list() {
91
+ return Object.entries(read()).map(([id, value]) => grantSummary(id, value));
92
+ },
93
+ withGrant(id, operation) {
94
+ idIsValid(id);
95
+ // The shared lock helper preserves pi credential path spellings; MCP locks the backing file itself.
96
+ return withLockedFile(grantFileTarget(file), async (raw, replace) => {
97
+ let records = decode(raw, file);
98
+ const save = (next) => {
99
+ replace(`${JSON.stringify(next)}\n`);
100
+ records = next;
101
+ };
102
+ let active = true;
103
+ const assertActive = () => {
104
+ if (!active)
105
+ throw new McpError("storage", "MCP grant access escaped its exclusive scope");
106
+ };
107
+ try {
108
+ const result = await operation({
109
+ read() {
110
+ assertActive();
111
+ return Object.hasOwn(records, id) ? checkedGrant(records[id]) : undefined;
112
+ },
113
+ async write(value) {
114
+ assertActive();
115
+ save({ ...records, [id]: checkedGrant(value) });
116
+ },
117
+ async remove() {
118
+ assertActive();
119
+ const next = { ...records };
120
+ delete next[id];
121
+ save(next);
122
+ },
123
+ });
124
+ return { result };
125
+ }
126
+ finally {
127
+ active = false;
128
+ }
129
+ }, { create: false });
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,19 @@
1
+ import { type McpAuthorization, type McpInspectOptions, type McpOptions, type McpServerSummary } from "../mcp-api.ts";
2
+ import { type McpServer } from "./config.ts";
3
+ /** The project's `.secrets/`: where the CLI keeps them. A write first makes the directory ignored by git. */
4
+ export declare function mcpAuthorization(dir: string, env: NodeJS.ProcessEnv, stateRoot: string, options?: McpOptions): McpAuthorization;
5
+ /** The instance's state directory: where an app that is given no store keeps them, beside its other state. */
6
+ export declare function mcpAuthorizationInState(stateRoot: string, options?: McpOptions): McpAuthorization;
7
+ export declare function writeMcpBinding(stateRoot: string, server: string, id: string | undefined): Promise<void>;
8
+ export declare function mcpContext(dir: string, options?: McpOptions): {
9
+ agentDir: string;
10
+ environment: {
11
+ [x: string]: string | undefined;
12
+ };
13
+ stateRoot: string;
14
+ auth: McpAuthorization;
15
+ };
16
+ export declare function httpServer(servers: readonly McpServer[], name: string): Extract<McpServer, {
17
+ type: "http";
18
+ }>;
19
+ export declare function inspectMcp(dir: string, options?: McpInspectOptions): Promise<McpServerSummary[]>;
@@ -0,0 +1,128 @@
1
+ /** MCP declarations and authorization bindings before an agent is opened. */
2
+ import { readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import * as Schema from "effect/Schema";
5
+ import { McpError, } from "../mcp-api.js";
6
+ import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
7
+ import { agentValues, prepareSecretsDirectory, unprotectedSecretsNote } from "../value-file.js";
8
+ import { log } from "../log.js";
9
+ import { withLockedFile } from "../locked-file.js";
10
+ import { checkedGrant, mcpGrantFile } from "./grants.js";
11
+ import { checkMcpUrl, declareMcp, mcpInstance } from "./config.js";
12
+ const Bindings = Schema.Record(Schema.String, Schema.String);
13
+ function decodeBindings(raw) {
14
+ if (raw === undefined)
15
+ return {};
16
+ try {
17
+ return Schema.decodeUnknownSync(Schema.fromJsonString(Bindings))(raw);
18
+ }
19
+ catch {
20
+ throw new McpError("storage", "Invalid MCP bindings file; refusing to overwrite it");
21
+ }
22
+ }
23
+ /** The project's `.secrets/`: where the CLI keeps them. A write first makes the directory ignored by git. */
24
+ export function mcpAuthorization(dir, env, stateRoot, options = {}) {
25
+ const secretsDir = resolveSecretsDir(dir, env);
26
+ return authorizationIn({ file: join(secretsDir, "mcp-auth.json"), prepare: () => prepareSecretsDirectory(dir, secretsDir) }, stateRoot, options);
27
+ }
28
+ /** The instance's state directory: where an app that is given no store keeps them, beside its other state. */
29
+ export function mcpAuthorizationInState(stateRoot, options = {}) {
30
+ return authorizationIn({ file: join(stateRoot, "mcp-auth.json") }, stateRoot, options);
31
+ }
32
+ function authorizationIn(home, stateRoot, options) {
33
+ if ((options.store === undefined) !== (options.bindings === undefined))
34
+ throw new McpError("configuration", "MCP store and bindings must be supplied together");
35
+ if (options.store && options.bindings)
36
+ return { store: options.store, bindings: { ...options.bindings } };
37
+ const path = join(stateRoot, "mcp-bindings.json");
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(path, "utf8");
41
+ }
42
+ catch (error) {
43
+ if (error.code !== "ENOENT")
44
+ throw error;
45
+ }
46
+ const file = mcpGrantFile(home.file);
47
+ const protectedWrite = async (write) => {
48
+ const unprotected = await home.prepare?.();
49
+ await write();
50
+ if (unprotected !== undefined)
51
+ log.warn(`[fastagent] ${unprotectedSecretsNote(unprotected)}`);
52
+ };
53
+ return {
54
+ store: {
55
+ ...file,
56
+ withGrant: (id, operation) => file.withGrant(id, (access) => operation({
57
+ ...access,
58
+ write: (record) => protectedWrite(() => access.write(record)),
59
+ remove: () => protectedWrite(() => access.remove()),
60
+ })),
61
+ },
62
+ bindings: decodeBindings(raw),
63
+ };
64
+ }
65
+ export async function writeMcpBinding(stateRoot, server, id) {
66
+ await withLockedFile(join(stateRoot, "mcp-bindings.json"), async (raw) => {
67
+ const entries = decodeBindings(raw);
68
+ if (id === undefined)
69
+ delete entries[server];
70
+ else
71
+ entries[server] = id;
72
+ return { result: undefined, next: `${JSON.stringify(entries)}\n` };
73
+ }, { create: false });
74
+ }
75
+ export function mcpContext(dir, options = {}) {
76
+ const agentDir = resolve(dir);
77
+ const environment = { ...process.env, ...agentValues(agentDir) };
78
+ const stateRoot = resolveStateRoot(agentDir, environment);
79
+ return {
80
+ agentDir,
81
+ environment,
82
+ stateRoot,
83
+ auth: mcpAuthorization(agentDir, environment, stateRoot, options),
84
+ };
85
+ }
86
+ export function httpServer(servers, name) {
87
+ const server = servers.find((s) => s.name === name);
88
+ if (server?.type !== "http")
89
+ throw new McpError("configuration", "OAuth needs a declared HTTP MCP server", name);
90
+ if (new Headers(server.headers).has("authorization"))
91
+ throw new McpError("configuration", "An MCP server cannot use a static Authorization header and an OAuth binding together", name);
92
+ return server;
93
+ }
94
+ export async function inspectMcp(dir, options = {}) {
95
+ const { agentDir, auth } = mcpContext(dir, options);
96
+ return Promise.all(declareMcp(agentDir, mcpInstance(options.config)).map(async ({ name, server: declaration }) => {
97
+ const server = { ...declaration, name, type: declaration.type ?? "stdio" };
98
+ const id = Object.hasOwn(auth.bindings, server.name) ? auth.bindings[server.name] : undefined;
99
+ const raw = id === undefined ? undefined : await auth.store.read(id);
100
+ const grant = raw === undefined ? undefined : checkedGrant(raw);
101
+ let authorization = "not_bound";
102
+ if (declaration.type !== "http")
103
+ authorization = id === undefined ? "not_required" : "binding_mismatch";
104
+ else if (grant) {
105
+ authorization =
106
+ grant.serverUrl !== checkMcpUrl(declaration.url).href || new Headers(declaration.headers).has("authorization")
107
+ ? "binding_mismatch"
108
+ : grant.status !== "ready"
109
+ ? "reauthorization_required"
110
+ : grant.expiresAt !== undefined && grant.expiresAt <= Date.now() + 30_000
111
+ ? grant.refreshToken
112
+ ? "refresh_needed"
113
+ : "reauthorization_required"
114
+ : "ready";
115
+ }
116
+ else if (id !== undefined)
117
+ authorization = "reauthorization_required";
118
+ else if (declaration.type === "http" && new Headers(declaration.headers).has("authorization"))
119
+ authorization = "not_required";
120
+ return {
121
+ name: server.name,
122
+ transport: server.type,
123
+ ...(declaration.type === "http" ? { url: checkMcpUrl(declaration.url).href } : {}),
124
+ authorization,
125
+ ...(id === undefined ? {} : { grantId: id }),
126
+ };
127
+ }));
128
+ }
@@ -0,0 +1,12 @@
1
+ export declare function checkMcpTarget(serverUrl: string, value: string): URL;
2
+ export declare function mcpFetch(serverUrl: string, fetchImpl: typeof globalThis.fetch, timeoutMs?: number): typeof globalThis.fetch;
3
+ type Lookup = (host: string) => Promise<readonly {
4
+ address: string;
5
+ family: number;
6
+ }[]>;
7
+ export interface McpNetwork {
8
+ fetch: typeof globalThis.fetch;
9
+ close(): Promise<void>;
10
+ }
11
+ export declare function openMcpNetwork(serverUrl: string, customFetch?: typeof globalThis.fetch, resolve?: Lookup): Promise<McpNetwork>;
12
+ export {};
@@ -0,0 +1,150 @@
1
+ /** MCP-derived URLs stay public unless they address the explicitly declared local endpoint. */
2
+ import { lookup } from "node:dns/promises";
3
+ import { BlockList, isIP } from "node:net";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Exit from "effect/Exit";
6
+ import { port, portError } from "../effect-port.js";
7
+ import { McpError } from "../mcp-api.js";
8
+ import { checkMcpUrl } from "./config.js";
9
+ const blocked = new BlockList();
10
+ for (const [address, prefix] of [
11
+ ["0.0.0.0", 8],
12
+ ["10.0.0.0", 8],
13
+ ["100.64.0.0", 10],
14
+ ["127.0.0.0", 8],
15
+ ["169.254.0.0", 16],
16
+ ["172.16.0.0", 12],
17
+ ["192.0.0.0", 24],
18
+ ["192.0.2.0", 24],
19
+ ["192.88.99.0", 24],
20
+ ["192.168.0.0", 16],
21
+ ["198.18.0.0", 15],
22
+ ["198.51.100.0", 24],
23
+ ["203.0.113.0", 24],
24
+ ["224.0.0.0", 4],
25
+ ["240.0.0.0", 4],
26
+ ])
27
+ blocked.addSubnet(address, prefix, "ipv4");
28
+ blocked.addAddress("168.63.129.16", "ipv4");
29
+ for (const [address, prefix] of [
30
+ ["2001::", 23],
31
+ ["2001:db8::", 32],
32
+ ["2002::", 16],
33
+ ["3fff::", 20],
34
+ ])
35
+ blocked.addSubnet(address, prefix, "ipv6");
36
+ const globalV6 = new BlockList();
37
+ globalV6.addSubnet("2000::", 3, "ipv6");
38
+ const mappedV4 = new BlockList();
39
+ mappedV4.addSubnet("0.0.0.0", 0, "ipv4");
40
+ const loopback = new BlockList();
41
+ loopback.addSubnet("127.0.0.0", 8, "ipv4");
42
+ loopback.addAddress("::1", "ipv6");
43
+ const hostname = (url) => url.hostname.replace(/^\[|\]$/g, "");
44
+ const localName = (name) => /(^|\.)localhost\.?$/i.test(name);
45
+ function allowedAddress(declared, target, address) {
46
+ const version = isIP(address);
47
+ if (!version)
48
+ return false;
49
+ const family = version === 4 ? "ipv4" : "ipv6";
50
+ if (declared.origin === target.origin &&
51
+ (hostname(declared) === address || (localName(hostname(declared)) && loopback.check(address, family))))
52
+ return true;
53
+ return (!blocked.check(address, family) &&
54
+ (version === 4 || mappedV4.check(address, family) || globalV6.check(address, family)));
55
+ }
56
+ function denied() {
57
+ throw new McpError("authorization_mismatch", "MCP network policy refused an undeclared non-public destination");
58
+ }
59
+ export function checkMcpTarget(serverUrl, value) {
60
+ const declared = checkMcpUrl(serverUrl);
61
+ const target = checkMcpUrl(value);
62
+ const host = hostname(target);
63
+ if (isIP(host) ? !allowedAddress(declared, target, host) : localName(host) && target.origin !== declared.origin)
64
+ denied();
65
+ return target;
66
+ }
67
+ export function mcpFetch(serverUrl, fetchImpl, timeoutMs = 30_000) {
68
+ return (input, init) => {
69
+ checkMcpTarget(serverUrl, input instanceof Request ? input.url : String(input));
70
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
71
+ return fetchImpl(input, {
72
+ ...init,
73
+ redirect: "error",
74
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
75
+ });
76
+ };
77
+ }
78
+ export async function openMcpNetwork(serverUrl, customFetch, resolve = (host) => lookup(host, { all: true, order: "ipv4first" })) {
79
+ if (customFetch)
80
+ return { fetch: mcpFetch(serverUrl, customFetch, 60_000), close: async () => { } };
81
+ // Bun's bare undici shim ignores dispatcher factories; load the installed implementation on both runtimes.
82
+ const { EnvHttpProxyAgent, Pool, fetch } = await import("undici/index.js");
83
+ const { noProxyList } = await import("../proxy.js");
84
+ const declared = checkMcpUrl(serverUrl);
85
+ const pins = new Map();
86
+ let closed = false;
87
+ const dispatcher = new EnvHttpProxyAgent({
88
+ noProxy: noProxyList(),
89
+ proxyTunnel: true,
90
+ factory: (origin, options) => {
91
+ const authority = new URL(origin);
92
+ const target = pins.get(authority.origin);
93
+ if (!target)
94
+ return denied();
95
+ // fetch removes Host; restore the original authority only after it selects this pinned pool.
96
+ return new Pool(target, options).compose((dispatch) => (request, handler) => dispatch({
97
+ ...request,
98
+ headers: Array.isArray(request.headers)
99
+ ? ["host", authority.host, ...request.headers]
100
+ : { ...request.headers, host: authority.host },
101
+ }, handler));
102
+ },
103
+ });
104
+ const request = async (input, init) => {
105
+ if (closed)
106
+ throw new McpError("connection", "MCP network is closed");
107
+ const request = new Request(input, init);
108
+ const url = checkMcpTarget(serverUrl, request.url);
109
+ if (!pins.has(url.origin)) {
110
+ const resolved = await Effect.runPromiseExit(port(async () => {
111
+ const host = hostname(url);
112
+ const addresses = isIP(host) ? [{ address: host, family: isIP(host) }] : await resolve(host);
113
+ const first = addresses[0];
114
+ if (!first || addresses.some(({ address }) => !allowedAddress(declared, url, address)))
115
+ return denied();
116
+ const pinned = new URL(url.origin);
117
+ const address = first.address;
118
+ pinned.hostname = isIP(address) === 6 ? `[${address}]` : address;
119
+ return pinned;
120
+ }), { signal: request.signal });
121
+ if (Exit.isFailure(resolved)) {
122
+ request.signal.throwIfAborted();
123
+ throw portError(resolved.cause);
124
+ }
125
+ pins.set(url.origin, resolved.value);
126
+ }
127
+ request.signal.throwIfAborted();
128
+ if (closed)
129
+ throw new McpError("connection", "MCP network is closed");
130
+ const headers = new Headers(request.headers);
131
+ if (headers.has("host") && headers.get("host") !== url.host)
132
+ throw new McpError("configuration", "MCP Host header must match the declared URL");
133
+ return (await fetch(request.url, {
134
+ dispatcher,
135
+ method: request.method,
136
+ headers: [...headers],
137
+ signal: request.signal,
138
+ body: request.body,
139
+ duplex: "half",
140
+ redirect: "error",
141
+ }));
142
+ };
143
+ return {
144
+ fetch: mcpFetch(serverUrl, request, 60_000),
145
+ close: async () => {
146
+ closed = true;
147
+ await dispatcher.destroy();
148
+ },
149
+ };
150
+ }
@@ -0,0 +1,2 @@
1
+ import { type McpGrantSummary, type McpSignInRequest } from "../mcp-api.ts";
2
+ export declare function signInMcp(request: McpSignInRequest): Promise<McpGrantSummary>;