@animalabs/agent-framework 0.22.4 → 0.23.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.
Files changed (234) hide show
  1. package/README.md +105 -3
  2. package/dist/src/agent.d.ts +36 -10
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +76 -32
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/code-execution/index.d.ts +1 -1
  7. package/dist/src/code-execution/index.d.ts.map +1 -1
  8. package/dist/src/code-execution/index.js.map +1 -1
  9. package/dist/src/code-execution/py-runner.d.ts +34 -2
  10. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  11. package/dist/src/code-execution/py-runner.js +72 -16
  12. package/dist/src/code-execution/py-runner.js.map +1 -1
  13. package/dist/src/code-execution/runtime-py.d.ts +3 -2
  14. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  15. package/dist/src/code-execution/runtime-py.js +47 -7
  16. package/dist/src/code-execution/runtime-py.js.map +1 -1
  17. package/dist/src/code-execution/tool-definition.d.ts +12 -7
  18. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  19. package/dist/src/code-execution/tool-definition.js +59 -11
  20. package/dist/src/code-execution/tool-definition.js.map +1 -1
  21. package/dist/src/config-checks.d.ts +2 -2
  22. package/dist/src/config-checks.d.ts.map +1 -1
  23. package/dist/src/config-checks.js +6 -2
  24. package/dist/src/config-checks.js.map +1 -1
  25. package/dist/src/framework.d.ts +744 -120
  26. package/dist/src/framework.d.ts.map +1 -1
  27. package/dist/src/framework.js +3546 -906
  28. package/dist/src/framework.js.map +1 -1
  29. package/dist/src/gate/event-gate.d.ts.map +1 -1
  30. package/dist/src/gate/event-gate.js +13 -6
  31. package/dist/src/gate/event-gate.js.map +1 -1
  32. package/dist/src/held-writes.d.ts +16 -0
  33. package/dist/src/held-writes.d.ts.map +1 -0
  34. package/dist/src/held-writes.js +56 -0
  35. package/dist/src/held-writes.js.map +1 -0
  36. package/dist/src/index.d.ts +7 -3
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +8 -4
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/mcpl/capability-grant.d.ts +4 -0
  41. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  42. package/dist/src/mcpl/capability-grant.js +1 -1
  43. package/dist/src/mcpl/capability-grant.js.map +1 -1
  44. package/dist/src/mcpl/channel-registry.d.ts +95 -18
  45. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  46. package/dist/src/mcpl/channel-registry.js +578 -150
  47. package/dist/src/mcpl/channel-registry.js.map +1 -1
  48. package/dist/src/mcpl/convert-block.d.ts +2 -2
  49. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  50. package/dist/src/mcpl/convert-block.js +6 -7
  51. package/dist/src/mcpl/convert-block.js.map +1 -1
  52. package/dist/src/mcpl/index.d.ts +5 -1
  53. package/dist/src/mcpl/index.d.ts.map +1 -1
  54. package/dist/src/mcpl/index.js +3 -1
  55. package/dist/src/mcpl/index.js.map +1 -1
  56. package/dist/src/mcpl/inference-router.d.ts +4 -2
  57. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  58. package/dist/src/mcpl/inference-router.js +6 -7
  59. package/dist/src/mcpl/inference-router.js.map +1 -1
  60. package/dist/src/mcpl/log-value.d.ts +7 -0
  61. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  62. package/dist/src/mcpl/log-value.js +9 -0
  63. package/dist/src/mcpl/log-value.js.map +1 -1
  64. package/dist/src/mcpl/modern-connection.d.ts +227 -0
  65. package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
  66. package/dist/src/mcpl/modern-connection.js +1088 -0
  67. package/dist/src/mcpl/modern-connection.js.map +1 -0
  68. package/dist/src/mcpl/prefix-rule.d.ts +18 -0
  69. package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
  70. package/dist/src/mcpl/prefix-rule.js +25 -0
  71. package/dist/src/mcpl/prefix-rule.js.map +1 -0
  72. package/dist/src/mcpl/protocol-family.d.ts +179 -0
  73. package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
  74. package/dist/src/mcpl/protocol-family.js +697 -0
  75. package/dist/src/mcpl/protocol-family.js.map +1 -0
  76. package/dist/src/mcpl/push-coalescer.d.ts +38 -19
  77. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  78. package/dist/src/mcpl/push-coalescer.js +52 -23
  79. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  80. package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
  81. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  82. package/dist/src/mcpl/reference-fetcher.js +243 -76
  83. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  84. package/dist/src/mcpl/references.d.ts +63 -18
  85. package/dist/src/mcpl/references.d.ts.map +1 -1
  86. package/dist/src/mcpl/references.js +117 -32
  87. package/dist/src/mcpl/references.js.map +1 -1
  88. package/dist/src/mcpl/server-connection.d.ts +326 -30
  89. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  90. package/dist/src/mcpl/server-connection.js +995 -212
  91. package/dist/src/mcpl/server-connection.js.map +1 -1
  92. package/dist/src/mcpl/server-registry.d.ts +25 -5
  93. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  94. package/dist/src/mcpl/server-registry.js +51 -12
  95. package/dist/src/mcpl/server-registry.js.map +1 -1
  96. package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
  97. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
  98. package/dist/src/mcpl/tool-result-normalize.js +232 -0
  99. package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
  100. package/dist/src/mcpl/transport.d.ts +56 -13
  101. package/dist/src/mcpl/transport.d.ts.map +1 -1
  102. package/dist/src/mcpl/transport.js +206 -70
  103. package/dist/src/mcpl/transport.js.map +1 -1
  104. package/dist/src/mcpl/types.d.ts +66 -20
  105. package/dist/src/mcpl/types.d.ts.map +1 -1
  106. package/dist/src/mcpl/types.js.map +1 -1
  107. package/dist/src/mcpl/visible-content.d.ts +2 -1
  108. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  109. package/dist/src/mcpl/visible-content.js +2 -1
  110. package/dist/src/mcpl/visible-content.js.map +1 -1
  111. package/dist/src/module-registry.d.ts +16 -6
  112. package/dist/src/module-registry.d.ts.map +1 -1
  113. package/dist/src/module-registry.js +65 -6
  114. package/dist/src/module-registry.js.map +1 -1
  115. package/dist/src/modules/api/index.d.ts +1 -1
  116. package/dist/src/modules/api/index.d.ts.map +1 -1
  117. package/dist/src/modules/api/index.js +3 -2
  118. package/dist/src/modules/api/index.js.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
  120. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  121. package/dist/src/modules/discord/discord-js-client.js +268 -65
  122. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  123. package/dist/src/modules/discord/index.d.ts +27 -5
  124. package/dist/src/modules/discord/index.d.ts.map +1 -1
  125. package/dist/src/modules/discord/index.js +366 -76
  126. package/dist/src/modules/discord/index.js.map +1 -1
  127. package/dist/src/modules/discord/refusal.d.ts +14 -0
  128. package/dist/src/modules/discord/refusal.d.ts.map +1 -0
  129. package/dist/src/modules/discord/refusal.js +14 -0
  130. package/dist/src/modules/discord/refusal.js.map +1 -0
  131. package/dist/src/modules/discord/types.d.ts +17 -7
  132. package/dist/src/modules/discord/types.d.ts.map +1 -1
  133. package/dist/src/modules/health/index.d.ts.map +1 -1
  134. package/dist/src/modules/health/index.js +6 -2
  135. package/dist/src/modules/health/index.js.map +1 -1
  136. package/dist/src/modules/index.d.ts +1 -1
  137. package/dist/src/modules/index.d.ts.map +1 -1
  138. package/dist/src/modules/index.js +1 -1
  139. package/dist/src/modules/index.js.map +1 -1
  140. package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
  141. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/disk-agreement.js +45 -5
  143. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  144. package/dist/src/modules/workspace/effects.d.ts +2 -1
  145. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/effects.js +19 -9
  147. package/dist/src/modules/workspace/effects.js.map +1 -1
  148. package/dist/src/modules/workspace/index.d.ts +214 -42
  149. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/index.js +667 -113
  151. package/dist/src/modules/workspace/index.js.map +1 -1
  152. package/dist/src/modules/workspace/observe.d.ts +240 -8
  153. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  154. package/dist/src/modules/workspace/observe.js +554 -24
  155. package/dist/src/modules/workspace/observe.js.map +1 -1
  156. package/dist/src/modules/workspace/types.d.ts +10 -1
  157. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  158. package/dist/src/modules/workspace/types.js.map +1 -1
  159. package/dist/src/modules/workspace/watcher.d.ts +42 -0
  160. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/watcher.js +130 -25
  162. package/dist/src/modules/workspace/watcher.js.map +1 -1
  163. package/dist/src/silent-heartbeat.d.ts +112 -39
  164. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  165. package/dist/src/silent-heartbeat.js +100 -29
  166. package/dist/src/silent-heartbeat.js.map +1 -1
  167. package/dist/src/tool-args.d.ts +40 -0
  168. package/dist/src/tool-args.d.ts.map +1 -0
  169. package/dist/src/tool-args.js +207 -0
  170. package/dist/src/tool-args.js.map +1 -0
  171. package/dist/src/tool-presentation.d.ts.map +1 -1
  172. package/dist/src/tool-presentation.js +9 -2
  173. package/dist/src/tool-presentation.js.map +1 -1
  174. package/dist/src/types/agent.d.ts +27 -17
  175. package/dist/src/types/agent.d.ts.map +1 -1
  176. package/dist/src/types/events.d.ts +8 -0
  177. package/dist/src/types/events.d.ts.map +1 -1
  178. package/dist/src/types/framework.d.ts +17 -2
  179. package/dist/src/types/framework.d.ts.map +1 -1
  180. package/dist/src/types/module.d.ts +54 -10
  181. package/dist/src/types/module.d.ts.map +1 -1
  182. package/dist/src/types/trace.d.ts +2 -2
  183. package/dist/src/types/trace.d.ts.map +1 -1
  184. package/package.json +3 -4
  185. package/src/agent.ts +99 -36
  186. package/src/code-execution/index.ts +1 -0
  187. package/src/code-execution/py-runner.ts +104 -17
  188. package/src/code-execution/runtime-py.ts +47 -7
  189. package/src/code-execution/tool-definition.ts +65 -11
  190. package/src/config-checks.ts +4 -2
  191. package/src/framework.ts +3633 -956
  192. package/src/gate/event-gate.ts +14 -6
  193. package/src/held-writes.ts +59 -0
  194. package/src/index.ts +19 -4
  195. package/src/mcpl/capability-grant.ts +1 -1
  196. package/src/mcpl/channel-registry.ts +613 -160
  197. package/src/mcpl/convert-block.ts +15 -7
  198. package/src/mcpl/index.ts +14 -1
  199. package/src/mcpl/inference-router.ts +8 -7
  200. package/src/mcpl/log-value.ts +10 -0
  201. package/src/mcpl/modern-connection.ts +1182 -0
  202. package/src/mcpl/prefix-rule.ts +32 -0
  203. package/src/mcpl/protocol-family.ts +691 -0
  204. package/src/mcpl/push-coalescer.ts +67 -29
  205. package/src/mcpl/reference-fetcher.ts +288 -82
  206. package/src/mcpl/references.ts +123 -33
  207. package/src/mcpl/server-connection.ts +1050 -221
  208. package/src/mcpl/server-registry.ts +50 -12
  209. package/src/mcpl/tool-result-normalize.ts +294 -0
  210. package/src/mcpl/transport.ts +204 -62
  211. package/src/mcpl/types.ts +67 -20
  212. package/src/mcpl/visible-content.ts +2 -1
  213. package/src/module-registry.ts +68 -13
  214. package/src/modules/api/index.ts +3 -2
  215. package/src/modules/discord/discord-js-client.ts +288 -72
  216. package/src/modules/discord/index.ts +380 -77
  217. package/src/modules/discord/refusal.ts +13 -0
  218. package/src/modules/discord/types.ts +17 -9
  219. package/src/modules/health/index.ts +6 -2
  220. package/src/modules/index.ts +1 -1
  221. package/src/modules/workspace/disk-agreement.ts +46 -5
  222. package/src/modules/workspace/effects.ts +18 -7
  223. package/src/modules/workspace/index.ts +722 -124
  224. package/src/modules/workspace/observe.ts +591 -22
  225. package/src/modules/workspace/types.ts +10 -1
  226. package/src/modules/workspace/watcher.ts +126 -25
  227. package/src/silent-heartbeat.ts +184 -59
  228. package/src/tool-args.ts +213 -0
  229. package/src/tool-presentation.ts +9 -2
  230. package/src/types/agent.ts +27 -17
  231. package/src/types/events.ts +8 -0
  232. package/src/types/framework.ts +17 -2
  233. package/src/types/module.ts +55 -10
  234. package/src/types/trace.ts +2 -2
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Which MCP protocol family a configured server speaks, and over which
3
+ * transport. Both are decided from the configuration alone, before anything
4
+ * is spawned or dialed: there is no cross-family probing and no fallback.
5
+ *
6
+ * - **Legacy**: MCP 2024-11-05, with MCPL riding on it as an experimental
7
+ * capability. `McplServerConnection` speaks it, over stdio or MCPL's
8
+ * WebSocket binding.
9
+ * - **Modern**: MCP 2026-07-28, which is stateless. It has no `initialize`;
10
+ * every request carries its version and client capabilities in `_meta`, and
11
+ * server-to-client requests are replaced by in-band continuation.
12
+ * `ModernMcpConnection` speaks it, over stdio or Streamable HTTP.
13
+ *
14
+ * A URL's scheme decides a network server's family: `ws://`/`wss://` is
15
+ * legacy, `http://`/`https://` is modern, because neither family has the
16
+ * other's binding. A stdio (`command`) server can be either. It is legacy
17
+ * unless its config says `protocol: 'modern'`, so every existing stdio
18
+ * configuration opens exactly as before.
19
+ *
20
+ * Why no auto-detection: the modern spec's discover-first probe makes some
21
+ * real MCPL servers exit (heartbeat-mcpl closes on any first message that
22
+ * isn't `initialize`), the SDK's stdio probe launches the server twice, and a
23
+ * server's era is a stable fact its configuration can state.
24
+ */
25
+ import { DENY_BY_DEFAULT, capabilityPatternMatches } from './capability-grant.js';
26
+ import { DEFAULT_EAGER_FETCH_MAX_BYTES, DEFAULT_MAX_TOTAL_BYTES } from './reference-fetcher.js';
27
+ import { DEFAULT_MAX_INPUT_BYTES } from './tool-lifecycle.js';
28
+ /** The one revision the legacy engine offers and accepts. */
29
+ export const LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
30
+ /** The revision the modern engine pins. */
31
+ export const MODERN_MCP_PROTOCOL_VERSION = '2026-07-28';
32
+ /** The largest delay a Node/Bun timer honors (2³¹−1 ms, about 24.8 days). */
33
+ export const MAX_TIMER_MS = 2_147_483_647;
34
+ /**
35
+ * A path segment that may be a credential. Some hosted servers put a
36
+ * per-user secret in the path (`…/mcp/s/<secret>/mcp`). A random token
37
+ * (base64url, hex, a uuid) is long and carries a digit; anything very long
38
+ * is treated as one too. A short or plain-word segment can't be told apart
39
+ * from a name, so it stays.
40
+ */
41
+ function credentialLikeSegment(segment) {
42
+ return (segment.length >= 16 && /[0-9]/.test(segment)) || segment.length >= 32;
43
+ }
44
+ /** Query parameter names that say they carry a credential. */
45
+ const CREDENTIAL_PARAM = /token|key|secret|password|auth|signature/i;
46
+ /**
47
+ * A query parameter name that may itself be a credential, which
48
+ * {@link redactUrl} replaces and a warning never quotes: one shaped like a
49
+ * token, or one with no value whose name says it carries a credential, since
50
+ * a bare token rides as a name alone (`?<token>`).
51
+ */
52
+ function credentialLikeQueryName(name, value) {
53
+ return credentialLikeSegment(name) || (value === '' && CREDENTIAL_PARAM.test(name));
54
+ }
55
+ /**
56
+ * A url as text a person or a log reads it: its scheme, host and port, and
57
+ * its path with any credential-like segment replaced. The userinfo is
58
+ * removed and each query parameter's value replaced, since a credential
59
+ * rides there too, and so is a parameter name that may itself be one
60
+ * ({@link credentialLikeQueryName}). A url that doesn't parse can't be
61
+ * redacted, so it isn't shown at all.
62
+ */
63
+ export function redactUrl(url) {
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(url ?? '');
67
+ }
68
+ catch {
69
+ return '(an invalid url, not shown)';
70
+ }
71
+ parsed.username = '';
72
+ parsed.password = '';
73
+ parsed.hash = '';
74
+ parsed.pathname = parsed.pathname.split('/').map((segment) => (credentialLikeSegment(segment) ? 'redacted' : segment)).join('/');
75
+ parsed.search = new URLSearchParams([...parsed.searchParams].map(([name, value]) => [credentialLikeQueryName(name, value) ? 'redacted' : name, 'redacted'])).toString();
76
+ return parsed.toString();
77
+ }
78
+ /**
79
+ * Server and transport text as the framework records it, with the strings a
80
+ * server's configuration knows to be credentials replaced, and those its
81
+ * dials sent:
82
+ * - from the url as each dial sends it ({@link urlAsDialed}), and decoded:
83
+ * the configured one, and each one dialed with an access provider's token
84
+ * ({@link noteDialSent}): credential-like path segments and
85
+ * query names, credential-shaped or credential-named query values, every
86
+ * other non-empty parameter as its whole name=value, and a WebSocket url's
87
+ * username and password, its userinfo as one unit before its @ (the
88
+ * username alone when there's no password), with no length floor, so a
89
+ * short password goes with its username and a short username alone goes
90
+ * too, and the Basic credential a WebSocket client sends for that userinfo;
91
+ * - the static token, and each token an access provider handed a dial for
92
+ * a header, raw, and percent- or form-encoded with either case of hex;
93
+ * - a stdio server's environment values whose names say they're
94
+ * credentials, raw, and percent-encoded with either case of hex.
95
+ * Each of these strings, from every source, the name=value pairs and
96
+ * userinfo units too, is also replaced as JSON encoders write it:
97
+ * JSON.stringify's escapes (a quote, a backslash, a control character), and
98
+ * with its slashes escaped too, as some encoders write them.
99
+ * A string replaced on its own has at least 8 characters, since a shorter
100
+ * one can't be told from ordinary text, and it is replaced wherever it
101
+ * occurs, after an escape or inside a longer token too. A query parameter is
102
+ * also replaced as its whole name=value, only where no letter or digit
103
+ * adjoins the pair, so v=2 leaves dev=2 alone (a JSON escape before it, such
104
+ * as \n, isn't one); a short value goes only that way. Every place one of
105
+ * these matches is replaced, overlapping places included, and the
106
+ * replacement is never scanned again; text that may come
107
+ * from several servers takes one pass over all their configurations'
108
+ * strings (serverCredentialPatterns), so strings of different servers that
109
+ * overlap are both replaced whole. A credential neither the configuration
110
+ * holds nor a dial of it sent, such as another origin's, is outside it, and
111
+ * so are other encodings of the ones it holds. JSON-RPC answers, and the
112
+ * error a host's own connect raises, pass as they are.
113
+ *
114
+ * A configuration that can't be read, such as one whose `env` is a getter
115
+ * that throws, says nothing of which strings to replace, so the whole text
116
+ * is withheld ({@link WITHHELD_TEXT}) rather than thrown on: a redaction
117
+ * runs where a throw has nowhere to go, such as inside a transport's 'error'
118
+ * listener.
119
+ */
120
+ export function redactServerText(text, config) {
121
+ return serverTextRedactor(config)(text);
122
+ }
123
+ /**
124
+ * {@link redactServerText} for one configuration, its strings found and its
125
+ * pattern built once, and again once a dial has sent something new
126
+ * ({@link noteDialSent}): for a connection's records, such as each stderr
127
+ * line. A build that can't read the configuration withholds that call's
128
+ * text, and the next call builds again.
129
+ */
130
+ export function serverTextRedactor(config) {
131
+ let built = -1;
132
+ let redact;
133
+ return (text) => {
134
+ const version = dialsSent.get(config)?.version ?? 0;
135
+ if (version !== built) {
136
+ let patterns;
137
+ try {
138
+ patterns = serverCredentialPatterns(config);
139
+ }
140
+ catch {
141
+ return WITHHELD_TEXT;
142
+ }
143
+ redact = credentialRedactor(patterns);
144
+ built = version;
145
+ }
146
+ return redact(text);
147
+ };
148
+ }
149
+ /** What a redaction gives in place of text when the configuration it
150
+ * redacts with can't be read ({@link redactServerText}). */
151
+ export const WITHHELD_TEXT = "[withheld: the server's configuration couldn't be read to redact this]";
152
+ /**
153
+ * What a configuration's dials sent that the configuration doesn't hold: the
154
+ * urls dialed with an access provider's token, and the provider's tokens,
155
+ * sent in a url or a header. Kept by the configuration object the dial read.
156
+ * For the dials that note anything, a WebSocket dial and an HTTP bearer, that
157
+ * is the object the framework records the server's text with. A provider can
158
+ * hand each dial a new token, so only the newest sixteen of each are kept,
159
+ * newest by when a dial last sent it.
160
+ */
161
+ const dialsSent = new WeakMap();
162
+ const DIAL_SENT_KEPT = 16;
163
+ /** Note what one dial sent: the url it dialed, or a token it sent in a
164
+ * header. That configuration's patterns cover it from then on. */
165
+ export function noteDialSent(config, sent) {
166
+ let entry = dialsSent.get(config);
167
+ if (!entry) {
168
+ entry = { urls: [], tokens: [], version: 0 };
169
+ dialsSent.set(config, entry);
170
+ }
171
+ const kept = entry;
172
+ const keep = (list, value) => {
173
+ if (!value)
174
+ return;
175
+ const at = list.indexOf(value);
176
+ if (at >= 0) {
177
+ // Sent again: it moves to the newest place, and the set is unchanged.
178
+ list.splice(at, 1);
179
+ list.push(value);
180
+ return;
181
+ }
182
+ list.push(value);
183
+ if (list.length > DIAL_SENT_KEPT)
184
+ list.shift();
185
+ kept.version += 1;
186
+ };
187
+ keep(kept.urls, sent.url);
188
+ keep(kept.tokens, sent.token);
189
+ }
190
+ /**
191
+ * The url a dial sends. A WebSocket dial sets the token as the `token`
192
+ * parameter, and setting it makes URLSearchParams write the whole query
193
+ * again, form-encoded (a space as `+`), so the url as sent can differ from
194
+ * the url as configured. An http(s) dial sends the url as configured.
195
+ */
196
+ export function urlAsDialed(url, token) {
197
+ const dialed = new URL(url);
198
+ if (token && (dialed.protocol === 'ws:' || dialed.protocol === 'wss:'))
199
+ dialed.searchParams.set('token', token);
200
+ return dialed;
201
+ }
202
+ /**
203
+ * The strings {@link redactServerText} replaces for one configuration, as
204
+ * patterns for {@link credentialRedactor}. Text that may come from several
205
+ * servers goes through one redactor over all their patterns, so a secret
206
+ * that begins inside another configuration's, or shares its start, can't
207
+ * leave any of that one in place.
208
+ */
209
+ export function serverCredentialPatterns(config) {
210
+ // Strings replaced wherever they occur (after an escape or inside a longer
211
+ // token too), and name=value pairs, replaced only where no letter or digit
212
+ // adjoins them, so a pair such as v=2 leaves dev=2 alone. A pair's value is
213
+ // also replaced on its own when it's a credential, wherever it occurs.
214
+ const standalones = new Set();
215
+ const pairs = new Set();
216
+ const userinfo = new Set();
217
+ const standalone = (value) => { standalones.add(value); };
218
+ const decode = (value, query = false) => {
219
+ try {
220
+ return decodeURIComponent(query ? value.replace(/\+/g, ' ') : value);
221
+ }
222
+ catch {
223
+ return value;
224
+ }
225
+ };
226
+ // Percent-escapes in lowercase hex, as some encoders write them.
227
+ const lowerEscapes = (value) => value.replace(/%[0-9A-F]{2}/g, (escape) => escape.toLowerCase());
228
+ // A string as written, and percent-encoded in either case of hex (a string
229
+ // encodeURIComponent refuses, such as one with an unpaired surrogate, has
230
+ // no encoded form). Its JSON forms come with every other string's, below.
231
+ const withEncoded = (value) => {
232
+ standalone(value);
233
+ let encoded;
234
+ try {
235
+ encoded = encodeURIComponent(value);
236
+ }
237
+ catch {
238
+ return; /* no percent-encoded form */
239
+ }
240
+ standalone(encoded);
241
+ standalone(lowerEscapes(encoded));
242
+ };
243
+ // A token as sent: raw in a header, and percent- or form-encoded where a
244
+ // transport puts it in a url.
245
+ const addToken = (token) => {
246
+ withEncoded(token);
247
+ const formEncoded = new URLSearchParams({ t: token }).toString().slice(2);
248
+ standalone(formEncoded);
249
+ standalone(lowerEscapes(formEncoded));
250
+ };
251
+ addToken(config.token ?? '');
252
+ // A stdio server's environment values whose names say they carry a
253
+ // credential: a part of the name that ends with a credential word
254
+ // (GITHUB_TOKEN, API_KEY, PGPASSWORD, ACCESSTOKEN), and not one where the
255
+ // word only begins or sits inside a part (GIT_AUTHOR_NAME, KEYBOARD_LAYOUT,
256
+ // TOKENIZERS_PARALLELISM).
257
+ for (const [name, value] of Object.entries(config.env ?? {})) {
258
+ // A JavaScript host can pass a value that isn't a string; only strings are derived.
259
+ if (typeof value === 'string' && name.split(/[_\-.]/).some((part) => CREDENTIAL_NAME_PART.test(part)))
260
+ withEncoded(value);
261
+ }
262
+ // A url as a dial sends it. Nothing quotes the configured url before a
263
+ // dial, so its form as configured needs no strings of its own.
264
+ const addUrl = (url) => {
265
+ for (const segment of url.pathname.split('/')) {
266
+ if (credentialLikeSegment(segment)) {
267
+ standalone(segment);
268
+ standalone(decode(segment));
269
+ }
270
+ }
271
+ for (const parameter of url.search.slice(1).split('&')) {
272
+ const equals = parameter.indexOf('=');
273
+ const encodedName = equals < 0 ? parameter : parameter.slice(0, equals);
274
+ const encodedValue = equals < 0 ? '' : parameter.slice(equals + 1);
275
+ const name = decode(encodedName, true);
276
+ const value = decode(encodedValue, true);
277
+ if (credentialLikeQueryName(name, value)) {
278
+ standalone(encodedName);
279
+ standalone(name);
280
+ }
281
+ if (CREDENTIAL_PARAM.test(name) || credentialLikeSegment(value)) {
282
+ standalone(encodedValue);
283
+ standalone(value);
284
+ }
285
+ if (value !== '') {
286
+ pairs.add(`${encodedName}=${encodedValue}`);
287
+ pairs.add(`${name}=${value}`);
288
+ }
289
+ }
290
+ if (url.protocol === 'ws:' || url.protocol === 'wss:') {
291
+ for (const value of [url.username, url.password]) {
292
+ standalone(value);
293
+ standalone(decode(value));
294
+ }
295
+ // The userinfo itself as one unit wherever it stands before an @ with
296
+ // no letter or digit just before it: username:password, or the
297
+ // username alone when there's no password, as a token written as the
298
+ // username is. So a password too short to replace on its own still
299
+ // goes with its username, and a short username alone goes too. The @
300
+ // and that boundary anchor the unit as a pair's boundaries do, so it
301
+ // has no length floor. And the Basic credential a WebSocket client
302
+ // sends for it. Each from the userinfo as written and as decoded.
303
+ for (const [username, password] of [[url.username, url.password], [decode(url.username), decode(url.password)]]) {
304
+ if (!username && !password)
305
+ continue;
306
+ userinfo.add(password ? `${username}:${password}` : username);
307
+ standalone(Buffer.from(`${username}:${password}`).toString('base64'));
308
+ }
309
+ }
310
+ };
311
+ // Read before the parse's try: a configuration that can't be read throws,
312
+ // so its redaction withholds the text rather than redact it without the
313
+ // url's strings.
314
+ const configuredUrl = config.url ?? '';
315
+ let url;
316
+ try {
317
+ url = new URL(configuredUrl);
318
+ }
319
+ catch { /* Still remove the static token. */ }
320
+ if (url)
321
+ addUrl(urlAsDialed(url, config.token));
322
+ // What the dials sent beyond that: urls dialed with an access provider's
323
+ // token, and its tokens sent in a header.
324
+ const sent = dialsSent.get(config);
325
+ for (const dialed of sent?.urls ?? []) {
326
+ try {
327
+ addUrl(new URL(dialed));
328
+ }
329
+ catch { /* a dialed url always parses */ }
330
+ }
331
+ for (const token of sent?.tokens ?? [])
332
+ addToken(token);
333
+ // Every string above, from every source, as written and as JSON encoders
334
+ // write it: with JSON.stringify's escapes (a quote, a backslash, a control
335
+ // character), and with its slashes escaped too, as some encoders write
336
+ // them. So a url's credential goes in JSON's forms as a token's does: in a
337
+ // log line that prints a server's JSON value as its JSON text, or in a
338
+ // server's JSON that echoes a decoded url secret. A string replaced on its
339
+ // own has at least 8 characters in each form it's replaced in.
340
+ const withJsonForms = (values, floor) => {
341
+ const forms = new Set();
342
+ for (const value of values) {
343
+ const json = JSON.stringify(value).slice(1, -1);
344
+ for (const written of [value, json]) {
345
+ for (const form of [written, written.replace(/\//g, '\\/')])
346
+ if (form.length >= floor)
347
+ forms.add(form);
348
+ }
349
+ }
350
+ return forms;
351
+ };
352
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
353
+ // Where a pair or userinfo unit may start: where no letter or digit
354
+ // adjoins it, at the text's start or after any other character, or after a
355
+ // JSON escape that ends in one (\b \f \n \r \t, \uXXXX), as a value's JSON
356
+ // text writes a control character before it.
357
+ const start = String.raw `(?<=^|[^A-Za-z0-9]|\\[bfnrt]|\\u[0-9A-Fa-f]{4})`;
358
+ return [
359
+ ...[...withJsonForms(standalones, 8)].map(value => ({ value, source: escape(value) })),
360
+ ...[...withJsonForms(pairs, 0)].map(value => ({ value, source: `${start}${escape(value)}(?![A-Za-z0-9])` })),
361
+ ...[...withJsonForms(userinfo, 0)].map(value => ({ value, source: `${start}${escape(value)}(?=@)` })),
362
+ ];
363
+ }
364
+ /**
365
+ * Every place one of the given patterns matches becomes `redacted`,
366
+ * overlapping places included: the matches are found in the text as given,
367
+ * a match may begin inside another, and each run of overlapping matches is
368
+ * replaced as one. So two strings that overlap, one's end being the other's
369
+ * start, are both replaced whole, and the replacement is never scanned.
370
+ */
371
+ export function credentialRedactor(patterns) {
372
+ if (patterns.length === 0)
373
+ return asText;
374
+ // Longest first, so at each place the longest string that matches there is
375
+ // the match.
376
+ const sources = [...patterns].sort((a, b) => b.value.length - a.value.length).map(({ source }) => source);
377
+ const pattern = new RegExp([...new Set(sources)].join('|'), 'g');
378
+ return (text) => {
379
+ const input = asText(text);
380
+ const spans = [];
381
+ pattern.lastIndex = 0;
382
+ for (let match = pattern.exec(input); match; match = pattern.exec(input)) {
383
+ const start = match.index;
384
+ const end = start + match[0].length;
385
+ const last = spans[spans.length - 1];
386
+ if (last && start < last[1])
387
+ last[1] = Math.max(last[1], end);
388
+ else
389
+ spans.push([start, end]);
390
+ // The next search starts one place on, not after this match, so a
391
+ // match that begins inside this one is found too.
392
+ pattern.lastIndex = start + 1;
393
+ }
394
+ let redacted = '';
395
+ let from = 0;
396
+ for (const [start, end] of spans) {
397
+ redacted += `${input.slice(from, start)}redacted`;
398
+ from = end;
399
+ }
400
+ return redacted + input.slice(from);
401
+ };
402
+ }
403
+ /** Any value as text: a value that can't be printed reads as a placeholder. */
404
+ export function asText(text) {
405
+ if (typeof text === 'string')
406
+ return text;
407
+ try {
408
+ return String(text);
409
+ }
410
+ catch {
411
+ return '[a value that cannot be printed]';
412
+ }
413
+ }
414
+ /** A part of an environment variable's name that ends with a credential word, in the singular or plural, with any digits after it. */
415
+ const CREDENTIAL_NAME_PART = /(?:tokens?|keys?|secrets?|passwords?|passwd|pass|passphrases?|passcodes?|pat|auth|authorization|authentication|bearer|cookies?|creds|signatures?|credentials?)\d*$/i;
416
+ /**
417
+ * A credential provider's failure as a log line may show it: the error's
418
+ * class and its message's length, never the text, which may quote the
419
+ * credential it failed to refresh.
420
+ */
421
+ export function describeCredentialError(error) {
422
+ if (!(error instanceof Error))
423
+ return `a thrown ${typeof error}`;
424
+ const length = error.message.length;
425
+ return `${error.constructor?.name || error.name || 'Error'}, ${length} character${length === 1 ? '' : 's'}`;
426
+ }
427
+ function urlScheme(config) {
428
+ let parsed;
429
+ try {
430
+ parsed = new URL(config.url);
431
+ }
432
+ catch {
433
+ // Not echoed: a url that doesn't parse can't be redacted.
434
+ throw new Error(`MCP server "${config.id}": invalid url (it isn't shown, since it may carry a credential)`);
435
+ }
436
+ return parsed.protocol;
437
+ }
438
+ /**
439
+ * Resolve a server's family and transport. Throws on a configuration that
440
+ * names no usable transport (for example an `ftp://` URL, or
441
+ * `transport: 'http'` with a WebSocket URL), or names two without saying
442
+ * which: `command` and `url` together need a `transport`, since one of them
443
+ * goes unused. (A host may layer a url and its transport over a file's
444
+ * command, so both with a `transport` stay valid.)
445
+ */
446
+ export function resolveServerBinding(config) {
447
+ if (config.command && config.url && config.transport === undefined) {
448
+ throw new Error(`MCP server "${config.id}": "command" and "url" are both set, and no "transport" says which one to use: ` +
449
+ `a server is either launched (command) or dialed (url)`);
450
+ }
451
+ const networkKind = () => {
452
+ const scheme = urlScheme(config);
453
+ if (scheme === 'ws:' || scheme === 'wss:')
454
+ return 'websocket';
455
+ if (scheme === 'http:' || scheme === 'https:')
456
+ return 'http';
457
+ throw new Error(`MCP server "${config.id}": url must be ws:// or wss:// (legacy MCPL) or http:// or https:// (modern MCP), got "${scheme}"`);
458
+ };
459
+ let transport;
460
+ if (config.transport === 'stdio' || (config.transport === undefined && config.command)) {
461
+ if (!config.command) {
462
+ throw new Error(`MCP server "${config.id}": the stdio transport requires "command"`);
463
+ }
464
+ transport = 'stdio';
465
+ }
466
+ else if (config.url) {
467
+ transport = networkKind();
468
+ if (config.transport !== undefined && config.transport !== transport) {
469
+ throw new Error(`MCP server "${config.id}": transport "${config.transport}" does not match url ${redactUrl(config.url)}`);
470
+ }
471
+ }
472
+ else if (config.transport !== undefined) {
473
+ throw new Error(`MCP server "${config.id}": transport "${config.transport}" requires "url"`);
474
+ }
475
+ else {
476
+ throw new Error(`MCP server "${config.id}": needs "command" (stdio) or "url" (WebSocket or HTTP)`);
477
+ }
478
+ if (transport === 'stdio')
479
+ return { family: config.protocol ?? 'legacy', transport };
480
+ return { family: transport === 'http' ? 'modern' : 'legacy', transport };
481
+ }
482
+ /** A configured value that would take effect, as opposed to an empty default. */
483
+ function isEffective(value) {
484
+ if (value === undefined || value === null || value === false)
485
+ return false;
486
+ if (Array.isArray(value))
487
+ return value.length > 0;
488
+ if (typeof value === 'object')
489
+ return Object.keys(value).length > 0;
490
+ return true;
491
+ }
492
+ function isPlainRecord(value) {
493
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
494
+ }
495
+ /**
496
+ * The parts of a configuration's MCPL policy whose values grant something,
497
+ * by path, such as `allowHostCommands`, `scopes.<set>.whitelist`,
498
+ * `enabledCapabilities: toolLifecycle.observe`, `toolLifecycle.maxInputBytes`
499
+ * or `autofetch.maxBytes`: each
500
+ * asks for behaviour only an MCPL server can have. A modern server has none,
501
+ * so such a value can't apply there, and is refused rather than ignored, so
502
+ * that an operator who chose it learns so.
503
+ *
504
+ * A value that only takes something away, or repeats a default, is already
505
+ * satisfied on a modern server and isn't listed: `enabledFeatureSets`, an
506
+ * allow-list over every declared set (`feature-set-manager.ts`);
507
+ * `enabledCapabilities` patterns that match no path denied by default, since
508
+ * elsewhere it is an allow-list too; `disabledFeatureSets`,
509
+ * `disabledCapabilities`, a scope's `blacklist`, `channelSubscription:
510
+ * 'manual'` (the default), `toolLifecycle` with only a `maxInputBytes` at
511
+ * or below its default or with an `observe` or `inputs` that isn't a policy
512
+ * object (which grants nothing), an `autofetch` bound at or below its
513
+ * default, and
514
+ * `shouldTriggerInference`, which only narrows which channel messages
515
+ * trigger inference. So a host that stamps a deny-list on every server it
516
+ * deploys can deploy a modern one. Empty values grant nothing either.
517
+ */
518
+ export function mcplPolicyGrants(config) {
519
+ const grants = [];
520
+ if (isEffective(config.allowHostCommands))
521
+ grants.push('allowHostCommands');
522
+ // An allow-list, except that a pattern matching a path denied by default
523
+ // grants it (computeGrant's explicitlyEnabled), named by the path.
524
+ const enabled = config.enabledCapabilities;
525
+ if (Array.isArray(enabled)) {
526
+ for (const path of DENY_BY_DEFAULT) {
527
+ if (enabled.some((pattern) => typeof pattern !== 'string' || capabilityPatternMatches(pattern, path))) {
528
+ grants.push(`enabledCapabilities: ${path}`);
529
+ }
530
+ }
531
+ }
532
+ else if (isEffective(enabled)) {
533
+ grants.push('enabledCapabilities');
534
+ }
535
+ if (config.channelSubscription !== 'manual' && isEffective(config.channelSubscription))
536
+ grants.push('channelSubscription');
537
+ if (isPlainRecord(config.scopes)) {
538
+ for (const [set, scope] of Object.entries(config.scopes)) {
539
+ if (isEffective(scope?.whitelist))
540
+ grants.push(`scopes.${set}.whitelist`);
541
+ }
542
+ }
543
+ else if (isEffective(config.scopes)) {
544
+ grants.push('scopes');
545
+ }
546
+ for (const key of ['observe', 'inputs']) {
547
+ if (isPlainRecord(config.toolLifecycle?.[key]))
548
+ grants.push(`toolLifecycle.${key}`);
549
+ }
550
+ // A larger input bound than the default asks for more than MCPL gives
551
+ // by default, as an autofetch bound above its default does.
552
+ const maxInputBytes = config.toolLifecycle?.maxInputBytes;
553
+ if (maxInputBytes !== undefined && maxInputBytes !== null && !(typeof maxInputBytes === 'number' && maxInputBytes <= DEFAULT_MAX_INPUT_BYTES)) {
554
+ grants.push('toolLifecycle.maxInputBytes');
555
+ }
556
+ const bounds = [['maxBytes', DEFAULT_EAGER_FETCH_MAX_BYTES], ['maxTotalBytes', DEFAULT_MAX_TOTAL_BYTES]];
557
+ for (const [key, ceiling] of bounds) {
558
+ const value = config.autofetch?.[key];
559
+ if (value !== undefined && value !== null && !(typeof value === 'number' && value <= ceiling))
560
+ grants.push(`autofetch.${key}`);
561
+ }
562
+ return grants;
563
+ }
564
+ /**
565
+ * Problems with a server configuration's protocol settings, as messages; empty
566
+ * when it is usable. Legacy configurations are checked only for what this
567
+ * design added, so every configuration that worked before still passes but
568
+ * one: `command` and `url` with no `transport`, which was launched as stdio
569
+ * with its url ignored, and is now refused as naming no transport to use.
570
+ */
571
+ export function serverConfigProblems(config) {
572
+ let binding;
573
+ try {
574
+ binding = resolveServerBinding(config);
575
+ }
576
+ catch (error) {
577
+ return [error.message];
578
+ }
579
+ const problems = [];
580
+ if (binding.transport === 'http') {
581
+ // fetch refuses a url with userinfo, and its error quotes the whole url,
582
+ // password included, into the connect failure's log line, trace and
583
+ // ops alert. A setting nothing could send is refused, not ignored.
584
+ const url = new URL(config.url);
585
+ if (url.username || url.password) {
586
+ problems.push(`MCP server "${config.id}": an http(s):// url can't carry userinfo (user:password@), since the HTTP client refuses such a url; give the credential as "token" or "accessProvider"`);
587
+ }
588
+ }
589
+ if (config.protocol !== undefined) {
590
+ if (config.protocol !== 'legacy' && config.protocol !== 'modern') {
591
+ problems.push(`MCP server "${config.id}": protocol must be 'legacy' or 'modern', got ${JSON.stringify(config.protocol)}`);
592
+ }
593
+ else if (binding.transport !== 'stdio') {
594
+ problems.push(`MCP server "${config.id}": "protocol" applies only to stdio (command) servers; the url's scheme decides a network server's family`);
595
+ }
596
+ }
597
+ const timeout = config.requestTimeoutMs;
598
+ if (binding.family === 'modern') {
599
+ if (timeout !== undefined && !(Number.isInteger(timeout) && timeout >= 1 && timeout <= MAX_TIMER_MS)) {
600
+ problems.push(`MCP server "${config.id}": requestTimeoutMs must be an integer from 1 to ${MAX_TIMER_MS} for a modern server (got ${String(timeout)}); every modern call has a deadline`);
601
+ }
602
+ for (const path of mcplPolicyGrants(config)) {
603
+ problems.push(`MCP server "${config.id}": "${path}" asks for MCPL behaviour, which a modern MCP server cannot negotiate`);
604
+ }
605
+ // A modern stdio server takes no credential: nothing would carry it.
606
+ if (binding.transport === 'stdio') {
607
+ for (const field of ['token', 'accessProvider']) {
608
+ if (config[field]) {
609
+ problems.push(`MCP server "${config.id}": "${field}" applies only to a network (url) server; a modern stdio server would ignore it`);
610
+ }
611
+ }
612
+ }
613
+ }
614
+ else if (typeof timeout === 'number' && timeout > MAX_TIMER_MS) {
615
+ // Never usable: Node runs a longer timer after about 1 ms, so every
616
+ // request timed out at once. Every value that worked (0 included) passes.
617
+ problems.push(`MCP server "${config.id}": requestTimeoutMs must be at most ${MAX_TIMER_MS} (got ${String(timeout)}), or 0 to disable the request timeout; a larger value times every request out at once`);
618
+ }
619
+ return problems;
620
+ }
621
+ /** A host a connection to which never leaves this machine, as the WHATWG URL
622
+ * parser gives it. The unspecified addresses count: a connection to 0.0.0.0
623
+ * or [::] reaches this host. So do 127.0.0.0/8 written as an IPv4-mapped
624
+ * IPv6 address ([::ffff:127.0.0.1] parses as [::ffff:7f00:1]) and a name
625
+ * with its root's trailing dot (localhost.). */
626
+ function isLoopbackHost(hostname) {
627
+ const host = hostname.toLowerCase().replace(/\.$/, '');
628
+ return host === 'localhost' || host.endsWith('.localhost') || host === '[::1]' || /^127(\.\d{1,3}){3}$/.test(host) ||
629
+ /^\[::ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}\]$/.test(host) || host === '0.0.0.0' || host === '[::]';
630
+ }
631
+ /**
632
+ * What is usable in a server configuration but worth an operator's attention,
633
+ * as messages; empty when there is nothing. A credential for an `http://` or
634
+ * `ws://` url whose host isn't loopback crosses the network in cleartext:
635
+ * `token` or `accessProvider` (in the `Authorization` header, or the
636
+ * WebSocket `?token=` query), and a secret the url carries itself (a ws://
637
+ * url's userinfo, a query parameter whose name says it's a credential, or
638
+ * whose value is shaped like one under a plain name, a query parameter name
639
+ * that may itself be one, or a path segment that looks like one, as
640
+ * {@link redactUrl} judges it). A name that may itself be a credential is
641
+ * counted, never quoted. That can be deliberate, on a private network or
642
+ * behind a TLS-terminating proxy, so it is a warning and never a refusal.
643
+ */
644
+ export function serverConfigWarnings(config) {
645
+ let transport;
646
+ try {
647
+ transport = resolveServerBinding(config).transport;
648
+ }
649
+ catch {
650
+ return []; // an unusable configuration is serverConfigProblems' to report
651
+ }
652
+ if (transport === 'stdio')
653
+ return [];
654
+ const url = new URL(config.url);
655
+ if ((url.protocol !== 'http:' && url.protocol !== 'ws:') || isLoopbackHost(url.hostname))
656
+ return [];
657
+ const carried = [];
658
+ if (config.token || config.accessProvider)
659
+ carried.push('its credential');
660
+ // An http:// url's userinfo is refused (serverConfigProblems); a ws://
661
+ // url sends it.
662
+ if (url.protocol === 'ws:' && (url.username || url.password))
663
+ carried.push("its url's userinfo");
664
+ if (url.pathname.split('/').some(credentialLikeSegment))
665
+ carried.push("a credential-like segment of its url's path");
666
+ const entries = [...url.searchParams];
667
+ const hidden = entries.filter(([name, value]) => credentialLikeQueryName(name, value)).length;
668
+ // A parameter carries a credential by its name, or by its value's shape,
669
+ // as redaction reads it; the name is shown only when it isn't one itself.
670
+ const params = [...new Set(entries.filter(([name, value]) => !credentialLikeQueryName(name, value) && (CREDENTIAL_PARAM.test(name) || credentialLikeSegment(value))).map(([name]) => name))];
671
+ if (params.length > 0) {
672
+ carried.push(`its url's query ${params.length === 1 ? 'parameter' : 'parameters'} ${params.map((name) => JSON.stringify(name)).join(', ')}`);
673
+ }
674
+ if (hidden > 0) {
675
+ carried.push(hidden === 1 ? 'a query parameter whose name looks like a credential' : `${hidden} query parameters whose names look like credentials`);
676
+ }
677
+ if (carried.length === 0)
678
+ return [];
679
+ const what = carried.length === 1 ? carried[0] : `${carried.slice(0, -1).join(', ')} and ${carried[carried.length - 1]}`;
680
+ const one = carried.length === 1 && params.length <= 1 && hidden <= 1;
681
+ const secure = url.protocol === 'http:' ? 'https' : 'wss';
682
+ return [
683
+ `MCP server "${config.id}": ${what} ${one ? 'goes' : 'go'} to ${url.host} unencrypted (${url.protocol}//), ` +
684
+ `where anything on the network path can read ${one ? 'it' : 'them'}; use ${secure}:// unless that network is trusted`,
685
+ ];
686
+ }
687
+ /** Throw one error listing every problem. Otherwise log each warning and
688
+ * return the resolved binding. Both engines admit a configuration here. */
689
+ export function checkServerConfig(config) {
690
+ const problems = serverConfigProblems(config);
691
+ if (problems.length > 0)
692
+ throw new Error(problems.join('; '));
693
+ for (const warning of serverConfigWarnings(config))
694
+ console.warn(`[mcp] ${warning}`);
695
+ return resolveServerBinding(config);
696
+ }
697
+ //# sourceMappingURL=protocol-family.js.map