@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,180 @@
1
+ /** OAuth discovery and refresh policy. Background calls never start an interactive sign-in. */
2
+ import { getSystemErrorMap } from "node:util";
3
+ import { checkResourceAllowed, discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata, extractWWWAuthenticateParams, refreshAuthorization, OAuthError, OAuthErrorCode, SdkError, SdkErrorCode, SdkHttpError, UnauthorizedError, InsufficientScopeError, IssuerMismatchError, } from "@modelcontextprotocol/client";
4
+ import { McpError } from "../mcp-api.js";
5
+ import { checkMcpTarget } from "./network.js";
6
+ import { checkedGrant } from "./grants.js";
7
+ export async function discoverMcp(url, fetchImpl) {
8
+ const challenge = await fetchImpl(url, { headers: { Accept: "application/json, text/event-stream" } });
9
+ await challenge.body?.cancel();
10
+ if (!challenge.ok && ![401, 403, 404, 405].includes(challenge.status))
11
+ throw new McpError("connection", `MCP resource probe failed (HTTP ${challenge.status})`);
12
+ const { resourceMetadataUrl } = extractWWWAuthenticateParams(challenge);
13
+ const hints = challenge.headers.get("www-authenticate")?.match(/\bresource_metadata\s*=/gi) ?? [];
14
+ if (hints.length > 1 || (hints.length === 1 && !resourceMetadataUrl))
15
+ throw new McpError("protocol", "Unusable MCP resource_metadata challenge; refusing default discovery");
16
+ if (resourceMetadataUrl)
17
+ checkMcpTarget(url, resourceMetadataUrl.href);
18
+ const resource = await discoverOAuthProtectedResourceMetadata(url, { resourceMetadataUrl }, fetchImpl);
19
+ const issuer = resource.authorization_servers?.[0] ?? new URL("/", url).href;
20
+ checkMcpTarget(url, issuer);
21
+ const metadata = await discoverAuthorizationServerMetadata(issuer, { fetchFn: fetchImpl });
22
+ if (!metadata ||
23
+ !resource ||
24
+ !checkResourceAllowed({ requestedResource: new URL(url), configuredResource: resource.resource })) {
25
+ throw new McpError("authorization_mismatch", "MCP authorization discovery did not identify the configured resource and issuer");
26
+ }
27
+ checkMcpTarget(url, metadata.issuer);
28
+ checkMcpTarget(url, metadata.authorization_endpoint);
29
+ checkMcpTarget(url, metadata.token_endpoint);
30
+ if (metadata.registration_endpoint)
31
+ checkMcpTarget(url, metadata.registration_endpoint);
32
+ return { metadata, resourceMetadata: resource, resource: resource.resource, issuer: metadata.issuer };
33
+ }
34
+ export function checkGrantTarget(record, url, discovery) {
35
+ if (record.serverUrl !== url ||
36
+ (discovery && (record.issuer !== discovery.issuer || record.resource !== discovery.resource))) {
37
+ throw new McpError("authorization_mismatch", "MCP grant belongs to a different endpoint, resource or issuer; sign in explicitly for this server");
38
+ }
39
+ }
40
+ const safeReasons = new Map([
41
+ ...getSystemErrorMap().values(),
42
+ ["ENOTFOUND", "DNS name not found"],
43
+ ["ERR_SSL_WRONG_VERSION_NUMBER", "TLS peer did not speak the expected protocol"],
44
+ ["UND_ERR_CONNECT_TIMEOUT", "connection timed out"],
45
+ ["UND_ERR_HEADERS_TIMEOUT", "response headers timed out"],
46
+ ["UND_ERR_BODY_TIMEOUT", "response body timed out"],
47
+ ["UND_ERR_SOCKET", "socket closed or failed"],
48
+ ["UND_ERR_CLOSED", "HTTP client closed"],
49
+ ["UND_ERR_DESTROYED", "HTTP client destroyed"],
50
+ ["ERR_TLS_CERT_ALTNAME_INVALID", "TLS certificate hostname mismatch"],
51
+ ["CERT_HAS_EXPIRED", "TLS certificate expired"],
52
+ ["DEPTH_ZERO_SELF_SIGNED_CERT", "untrusted self-signed TLS certificate"],
53
+ ["SELF_SIGNED_CERT_IN_CHAIN", "untrusted TLS certificate chain"],
54
+ ["UNABLE_TO_VERIFY_LEAF_SIGNATURE", "TLS certificate could not be verified"],
55
+ ["UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "TLS certificate issuer is not trusted"],
56
+ ]);
57
+ const sdkCodes = new Set([...Object.values(SdkErrorCode), ...Object.values(OAuthErrorCode)]);
58
+ const errorNames = new Set([
59
+ "Error",
60
+ "TypeError",
61
+ "SyntaxError",
62
+ "RangeError",
63
+ "ReferenceError",
64
+ "URIError",
65
+ "EvalError",
66
+ "AbortError",
67
+ "TimeoutError",
68
+ "AggregateError",
69
+ "OAuthError",
70
+ "SdkError",
71
+ ]);
72
+ function safeFailureReason(error) {
73
+ const pending = [error];
74
+ const seen = new Set();
75
+ const reasons = new Set();
76
+ while (pending.length) {
77
+ const current = pending.pop();
78
+ if (!(current instanceof Error) || seen.has(current))
79
+ continue;
80
+ seen.add(current);
81
+ const code = "code" in current && typeof current.code === "string" ? current.code : undefined;
82
+ if (code !== undefined) {
83
+ const reason = safeReasons.get(code);
84
+ if (reason !== undefined)
85
+ reasons.add(`${code}: ${reason}`);
86
+ else if (sdkCodes.has(code))
87
+ reasons.add(code);
88
+ }
89
+ // SDK 2.4's pinned stdio probe omits structured close data; recognize only its fixed diagnostic prefix.
90
+ if (current.message === "Connection closed during the version negotiation probe" ||
91
+ (current instanceof SdkError &&
92
+ current.code === SdkErrorCode.EraNegotiationFailed &&
93
+ current.message.startsWith("Version negotiation failed: the connection closed during the server/discover probe before the server offered pinned protocol version ")))
94
+ reasons.add("CONNECTION_CLOSED: server closed during protocol discovery");
95
+ if (current instanceof SdkHttpError &&
96
+ Number.isInteger(current.status) &&
97
+ current.status >= 100 &&
98
+ current.status <= 599)
99
+ reasons.add(`HTTP ${current.status}`);
100
+ pending.push(current.cause);
101
+ if (current instanceof AggregateError && Array.isArray(current.errors))
102
+ pending.push(...current.errors);
103
+ }
104
+ if (reasons.size)
105
+ return [...reasons].join("; ");
106
+ return error instanceof Error ? (errorNames.has(error.name) ? error.name : "Error") : "unknown error";
107
+ }
108
+ export function mcpFailure(error, operation, server) {
109
+ if (error instanceof McpError)
110
+ return error.server || !server ? error : new McpError(error.code, error.message, server);
111
+ if (error instanceof IssuerMismatchError)
112
+ return new McpError("authorization_mismatch", `${operation}: OAuth issuer validation failed`, server);
113
+ if (error instanceof UnauthorizedError || (error instanceof SdkHttpError && error.status === 401))
114
+ return new McpError("authorization_required", `${operation}: MCP authorization required; sign in for this server`, server);
115
+ if (error instanceof InsufficientScopeError || (error instanceof SdkHttpError && error.status === 403))
116
+ return new McpError("insufficient_scope", `${operation}: MCP access refused (HTTP 403); review the grant permissions`, server);
117
+ // Error messages, paths, args, stderr and raw causes can contain credentials; only allowlisted facts escape.
118
+ return new McpError("connection", `${operation} failed (${safeFailureReason(error)})`, server);
119
+ }
120
+ export async function grantToken(input) {
121
+ return input.store.withGrant(input.id, async (access) => {
122
+ const raw = access.read();
123
+ if (!raw)
124
+ throw new McpError("authorization_required", "No MCP grant is stored; sign in before opening this agent");
125
+ const record = checkedGrant(raw);
126
+ checkGrantTarget(record, input.url, input.discovery);
127
+ if (record.status !== "ready")
128
+ throw new McpError(record.status === "refreshing" ? "refresh_uncertain" : "authorization_required", "MCP authorization must be renewed; the previous grant cannot be refreshed safely");
129
+ const fresh = record.expiresAt === undefined || record.expiresAt > Date.now() + 30_000;
130
+ if (fresh && (input.rejectedGeneration === undefined || record.generation !== input.rejectedGeneration)) {
131
+ return { token: record.accessToken, generation: record.generation };
132
+ }
133
+ if (!record.refreshToken) {
134
+ await access.write({ ...record, status: "reauthorization_required" });
135
+ throw new McpError("authorization_required", "MCP authorization expired and has no refresh token; sign in again");
136
+ }
137
+ const discovery = await discoverMcp(input.url, input.fetch);
138
+ checkGrantTarget(record, input.url, discovery);
139
+ // Persist before the remote server can consume the old token. A lost response cannot be replayed safely.
140
+ await access.write({ ...record, status: "refreshing" });
141
+ let tokens;
142
+ try {
143
+ tokens = await refreshAuthorization(record.issuer, {
144
+ metadata: discovery.metadata,
145
+ clientInformation: {
146
+ client_id: record.client.id,
147
+ ...(record.client.secret === undefined ? {} : { client_secret: record.client.secret }),
148
+ token_endpoint_auth_method: record.client.tokenEndpointAuthMethod,
149
+ },
150
+ refreshToken: record.refreshToken,
151
+ resource: record.resource,
152
+ fetchFn: input.fetch,
153
+ });
154
+ }
155
+ catch (error) {
156
+ if (error instanceof OAuthError &&
157
+ ["invalid_grant", "invalid_client", "unauthorized_client"].includes(error.code)) {
158
+ await access.write({ ...record, status: "reauthorization_required" });
159
+ throw new McpError("authorization_required", "The MCP authorization server refused the refresh; sign in again");
160
+ }
161
+ throw mcpFailure(error, "MCP token refresh");
162
+ }
163
+ const scopes = tokens.scope === undefined ? record.scopes : tokens.scope.split(/\s+/).filter(Boolean);
164
+ if (scopes.some((scope) => !record.scopes.includes(scope)))
165
+ throw new McpError("insufficient_scope", "MCP refresh unexpectedly expanded the grant's scopes");
166
+ const next = {
167
+ ...record,
168
+ scopes,
169
+ status: "ready",
170
+ generation: record.generation + 1,
171
+ accessToken: tokens.access_token,
172
+ ...(tokens.refresh_token === undefined ? {} : { refreshToken: tokens.refresh_token }),
173
+ expiresAt: tokens.expires_in === undefined ? undefined : Date.now() + tokens.expires_in * 1000,
174
+ };
175
+ if (tokens.token_type.toLowerCase() !== "bearer")
176
+ throw new McpError("protocol", "Only bearer MCP grants are supported");
177
+ await access.write(next);
178
+ return { token: next.accessToken, generation: next.generation };
179
+ });
180
+ }
@@ -0,0 +1,15 @@
1
+ /** Instance-owned MCP clients. The scope also joins refreshes after a caller abandons its wait. */
2
+ import { type Tool, type CallToolResult } from "@modelcontextprotocol/client";
3
+ import { type McpAuthorization, type McpOptions } from "../mcp-api.ts";
4
+ import type { McpServer } from "./config.ts";
5
+ export interface McpTool {
6
+ name: string;
7
+ server: string;
8
+ definition: Tool;
9
+ call(args: Record<string, unknown>, signal?: AbortSignal, progress?: (text: string) => void): Promise<CallToolResult>;
10
+ }
11
+ export interface McpClients {
12
+ tools: McpTool[];
13
+ close(): Promise<void>;
14
+ }
15
+ export declare function openMcpClients(servers: readonly McpServer[], auth: McpAuthorization, options?: Pick<McpOptions, "fetch">): Promise<McpClients>;
@@ -0,0 +1,179 @@
1
+ /** Instance-owned MCP clients. The scope also joins refreshes after a caller abandons its wait. */
2
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
3
+ import { DEFAULT_INHERITED_ENV_VARS, StdioClientTransport } from "@modelcontextprotocol/client/stdio";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Exit from "effect/Exit";
6
+ import * as Fiber from "effect/Fiber";
7
+ import * as Scope from "effect/Scope";
8
+ import { portAbort, portJoin, portError } from "../effect-port.js";
9
+ import { McpError } from "../mcp-api.js";
10
+ import { checkGrantTarget, discoverMcp, grantToken, mcpFailure } from "./auth.js";
11
+ import { mcpFetch, openMcpNetwork } from "./network.js";
12
+ import { checkedGrant } from "./grants.js";
13
+ import { log } from "../log.js";
14
+ /**
15
+ * The SDK starts a stdio server over `{ ...getDefaultEnvironment(), ...env }`, which adds this process's own `HOME`,
16
+ * `PATH`, `USER` and the like under whatever is given. Naming each of them with no value, where `env` holds none,
17
+ * leaves the server with exactly `env`: Node drops a variable whose value is undefined.
18
+ */
19
+ function ownEnvironment(env) {
20
+ const withheld = Object.fromEntries(DEFAULT_INHERITED_ENV_VARS.filter((name) => !(name in env)).map((name) => [name, undefined]));
21
+ return { ...withheld, ...env };
22
+ }
23
+ export async function openMcpClients(servers, auth, options = {}) {
24
+ const lifetime = Scope.makeUnsafe();
25
+ const cleanupErrors = [];
26
+ const tools = [];
27
+ const names = new Set();
28
+ let closing;
29
+ const run = async (work) => {
30
+ if (closing)
31
+ throw new McpError("connection", "MCP clients are closed");
32
+ const result = await Effect.runPromiseExit(Effect.forkIn(work, lifetime).pipe(Effect.flatMap(Fiber.join)));
33
+ if (Exit.isFailure(result))
34
+ throw portError(result.cause);
35
+ return result.value;
36
+ };
37
+ const close = () => (closing ??= (async () => {
38
+ await Effect.runPromise(Scope.close(lifetime, Exit.void));
39
+ if (cleanupErrors.length)
40
+ throw new AggregateError(cleanupErrors, "MCP cleanup failed");
41
+ })());
42
+ let openingServer;
43
+ try {
44
+ for (const server of servers) {
45
+ openingServer = server.name;
46
+ const client = new Client({ name: "FastAgent", version: "1" }, { versionNegotiation: { mode: { pin: "2026-07-28" }, probe: { timeoutMs: 15_000, maxRetries: 0 } } });
47
+ await Effect.runPromise(Scope.addFinalizer(lifetime, portJoin(() => client.close()).pipe(Effect.catchCause((cause) => Effect.sync(() => {
48
+ cleanupErrors.push(mcpFailure(portError(cause), "MCP cleanup", server.name));
49
+ })))));
50
+ client.onerror = (error) => log.warn(`[mcp] ${mcpFailure(error, "Protocol error", server.name).message}`);
51
+ let disconnected = false;
52
+ client.onclose = () => {
53
+ disconnected = true;
54
+ };
55
+ const grantId = Object.hasOwn(auth.bindings, server.name) ? auth.bindings[server.name] : undefined;
56
+ let transport;
57
+ if (server.type === "stdio") {
58
+ if (grantId !== undefined)
59
+ throw new McpError("configuration", "A stdio MCP server cannot have an OAuth grant binding", server.name);
60
+ transport = new StdioClientTransport({
61
+ command: server.command,
62
+ args: server.args,
63
+ cwd: server.cwd,
64
+ env: ownEnvironment(server.env),
65
+ stderr: "pipe",
66
+ });
67
+ // Draining prevents a noisy child from blocking. Raw stderr can contain its supplied secrets.
68
+ transport.stderr?.on("data", () => { });
69
+ }
70
+ else {
71
+ const network = await openMcpNetwork(server.url, options.fetch);
72
+ await Effect.runPromise(Scope.addFinalizer(lifetime, portJoin(() => network.close()).pipe(Effect.catchCause((cause) => Effect.sync(() => {
73
+ cleanupErrors.push(mcpFailure(portError(cause), "MCP network cleanup", server.name));
74
+ })))));
75
+ const fetchImpl = mcpFetch(server.url, network.fetch);
76
+ const requestFetch = mcpFetch(server.url, network.fetch, 60_000);
77
+ if (grantId !== undefined && new Headers(server.headers).has("authorization"))
78
+ throw new McpError("configuration", "Static Authorization and OAuth cannot be combined", server.name);
79
+ if (grantId !== undefined) {
80
+ const held = await auth.store.read(grantId);
81
+ if (!held)
82
+ throw new McpError("authorization_required", "No grant found for this MCP binding; sign in again", server.name);
83
+ checkGrantTarget(checkedGrant(held), server.url, await discoverMcp(server.url, fetchImpl));
84
+ }
85
+ const request = (input, init) => run(portJoin(async () => {
86
+ if (grantId === undefined)
87
+ return requestFetch(input, init);
88
+ const url = input instanceof Request ? input.url : String(input);
89
+ if (url !== server.url)
90
+ throw new McpError("authorization_mismatch", "Refusing to send an MCP token to another URL", server.name);
91
+ const token = await grantToken({ store: auth.store, id: grantId, url: server.url, fetch: fetchImpl });
92
+ init?.signal?.throwIfAborted();
93
+ const headers = new Headers(init?.headers);
94
+ headers.set("Authorization", `Bearer ${token.token}`);
95
+ const response = await requestFetch(input, { ...init, headers });
96
+ if (response.status !== 401)
97
+ return response;
98
+ await response.body?.cancel();
99
+ const refreshed = await grantToken({
100
+ store: auth.store,
101
+ id: grantId,
102
+ url: server.url,
103
+ fetch: fetchImpl,
104
+ rejectedGeneration: token.generation,
105
+ });
106
+ init?.signal?.throwIfAborted();
107
+ headers.set("Authorization", `Bearer ${refreshed.token}`);
108
+ const retry = await requestFetch(input, { ...init, headers });
109
+ if (retry.status === 401) {
110
+ await retry.body?.cancel();
111
+ await auth.store.withGrant(grantId, async (access) => {
112
+ const current = access.read();
113
+ if (current?.generation === refreshed.generation)
114
+ await access.write({ ...current, status: "reauthorization_required" });
115
+ });
116
+ throw new McpError("authorization_required", "MCP server refused the refreshed grant; sign in again", server.name);
117
+ }
118
+ return retry;
119
+ }));
120
+ transport = new StreamableHTTPClientTransport(new URL(server.url), {
121
+ fetch: request,
122
+ requestInit: { headers: server.headers },
123
+ onInsufficientScope: "throw",
124
+ });
125
+ }
126
+ // Register the transport separately: SDK discovery can fail before Client takes ownership of it.
127
+ await Effect.runPromise(Scope.addFinalizer(lifetime, portJoin(() => transport.close()).pipe(Effect.catchCause((cause) => Effect.sync(() => {
128
+ cleanupErrors.push(mcpFailure(portError(cause), "MCP transport cleanup", server.name));
129
+ })))));
130
+ await client.connect(transport, { timeout: 30_000 });
131
+ if (!client.getServerCapabilities()?.tools)
132
+ throw new McpError("protocol", "MCP server does not offer tools; other capabilities are not supported", server.name);
133
+ const listed = await client.listTools(undefined, { timeout: 30_000 });
134
+ for (const definition of listed.tools) {
135
+ const name = `mcp__${server.name}__${definition.name}`;
136
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) || names.has(name))
137
+ throw new McpError("configuration", `Invalid or duplicate MCP tool name ${name}`, server.name);
138
+ names.add(name);
139
+ tools.push({
140
+ name,
141
+ server: server.name,
142
+ definition,
143
+ async call(args, signal, progress) {
144
+ if (disconnected)
145
+ throw new McpError("connection", "MCP server disconnected; reopen the agent to reconnect", server.name);
146
+ const abort = new AbortController();
147
+ const forwarded = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
148
+ try {
149
+ return await run(portAbort("MCP tool", () => client.callTool({ name: definition.name, arguments: args }, {
150
+ signal: forwarded,
151
+ timeout: 60_000,
152
+ ...(progress
153
+ ? {
154
+ onprogress: (value) => progress(`MCP ${server.name}: ${value.progress}${value.total === undefined ? "" : `/${value.total}`}`),
155
+ }
156
+ : {}),
157
+ }), () => abort.abort()));
158
+ }
159
+ catch (error) {
160
+ if (forwarded.aborted)
161
+ throw forwarded.reason;
162
+ throw mcpFailure(error, "MCP tool call", server.name);
163
+ }
164
+ },
165
+ });
166
+ }
167
+ }
168
+ return { tools, close };
169
+ }
170
+ catch (error) {
171
+ try {
172
+ await close();
173
+ }
174
+ catch (cleanup) {
175
+ throw new AggregateError([mcpFailure(error, "MCP open", openingServer), cleanup], "MCP open and cleanup failed");
176
+ }
177
+ throw mcpFailure(error, "MCP open", openingServer);
178
+ }
179
+ }
@@ -0,0 +1,86 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { type DeclaredSecret } from "../declared-secrets.ts";
3
+ import { type McpConfigInput } from "../mcp-api.ts";
4
+ declare const Declaration: Schema.Union<readonly [Schema.Struct<{
5
+ readonly type: Schema.optional<Schema.Literal<"stdio">>;
6
+ readonly command: Schema.NonEmptyString;
7
+ readonly args: Schema.optional<Schema.$Array<Schema.String>>;
8
+ readonly env: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
9
+ }>, Schema.Struct<{
10
+ readonly type: Schema.Literal<"http">;
11
+ readonly url: Schema.String;
12
+ readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
13
+ }>]>;
14
+ /** One server as an `mcpServers` map holds it. */
15
+ export type McpServerDeclaration = typeof Declaration.Type;
16
+ export type McpServer = {
17
+ name: string;
18
+ type: "stdio";
19
+ command: string;
20
+ args: string[];
21
+ env: Record<string, string>;
22
+ cwd: string;
23
+ } | {
24
+ name: string;
25
+ type: "http";
26
+ url: string;
27
+ headers: Record<string, string>;
28
+ };
29
+ /** A server and where it was declared: what its refusals name, and what a relative command is relative to. */
30
+ export interface McpDeclaration {
31
+ name: string;
32
+ server: McpServerDeclaration;
33
+ /** The file that declared it, as a refusal names it. */
34
+ file: string;
35
+ /** The directory that file is in. None for an object, whose commands with a path must be absolute. */
36
+ base?: string;
37
+ origin: "program" | "instance";
38
+ }
39
+ /** The instance configuration as far as MCP reads it: its `mcpServers`, and where it is from. */
40
+ export interface McpInstance {
41
+ values: unknown;
42
+ label: string;
43
+ directory?: string;
44
+ }
45
+ /** An instance's servers, checked: what `resolveConfig` asks of a configuration, so its refusal is the file's. */
46
+ export declare function checkInstanceServers(instance: McpInstance): void;
47
+ /**
48
+ * The servers of the program in `dir` and of `instance`. An instance only adds: a name both declare is refused, naming
49
+ * both, so nothing is shadowed.
50
+ */
51
+ export declare function declareMcp(dir: string, instance?: McpInstance): McpDeclaration[];
52
+ /** An instance configuration given to `inspectMcp` or `signInMcp`: an object, or the absolute path of a `fastagent.json`. */
53
+ export declare function mcpInstance(config: McpConfigInput | undefined): McpInstance | undefined;
54
+ /** The secrets the servers' `${NAME}` references need: each required, declared where the server is. */
55
+ export declare function mcpSecrets(declarations: readonly McpDeclaration[]): DeclaredSecret[];
56
+ /**
57
+ * The header and environment values with no `${VAR}` in them: the format allows a literal credential, and where the
58
+ * file travels (a build publishes it, a deploy bakes or delivers it) each is said. FastAgent cannot tell a secret from a
59
+ * setting, so a caller warns and never refuses.
60
+ */
61
+ export declare function literalCredentials(declarations: readonly McpDeclaration[]): {
62
+ file: string;
63
+ server: string;
64
+ field: "headers" | "env";
65
+ key: string;
66
+ }[];
67
+ /**
68
+ * The stdio servers whose command is a path relative to the file that declares it, as it is written: what a build or a
69
+ * deploy has to be able to deliver, since a script is not found on a `PATH`. A command that starts with a `${VAR}` is
70
+ * left to the variable, and `args` are never read as paths, since FastAgent cannot know which strings are.
71
+ */
72
+ export declare function relativeCommands(declarations: readonly McpDeclaration[]): (McpDeclaration & {
73
+ command: string;
74
+ })[];
75
+ /** What to write instead of a literal credential, which a header's value usually carries a scheme in front of. */
76
+ export declare function literalCredentialAdvice({ field, key }: {
77
+ field: "headers" | "env";
78
+ key: string;
79
+ }): string;
80
+ export declare function checkMcpUrl(value: string, server?: string, file?: string): URL;
81
+ export declare function resolveMcpServers(declarations: readonly McpDeclaration[], env: Readonly<Record<string, string | undefined>>, only: string | undefined,
82
+ /** Where a stdio server runs: the instance's workspace, whoever declared it. */
83
+ cwd: string,
84
+ /** What every process the instance starts gets (child-env.ts), under a stdio server's own `env`. */
85
+ processEnv: Readonly<Record<string, string>>): McpServer[];
86
+ export {};