alepha 0.20.3 → 0.20.4

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 (217) hide show
  1. package/dist/api/audits/index.d.ts.map +1 -1
  2. package/dist/api/files/index.d.ts.map +1 -1
  3. package/dist/api/jobs/index.d.ts +14 -14
  4. package/dist/api/jobs/index.d.ts.map +1 -1
  5. package/dist/api/keys/index.d.ts +4 -4
  6. package/dist/api/organizations/index.d.ts.map +1 -1
  7. package/dist/api/parameters/index.d.ts +8 -3
  8. package/dist/api/parameters/index.d.ts.map +1 -1
  9. package/dist/api/parameters/index.js +20 -4
  10. package/dist/api/parameters/index.js.map +1 -1
  11. package/dist/api/payments/index.d.ts.map +1 -1
  12. package/dist/api/users/index.browser.js +6 -0
  13. package/dist/api/users/index.browser.js.map +1 -1
  14. package/dist/api/users/index.d.ts +5037 -139
  15. package/dist/api/users/index.d.ts.map +1 -1
  16. package/dist/api/users/index.js +58 -10
  17. package/dist/api/users/index.js.map +1 -1
  18. package/dist/bucket/index.d.ts +77 -107
  19. package/dist/bucket/index.d.ts.map +1 -1
  20. package/dist/bucket/index.js +148 -4
  21. package/dist/bucket/index.js.map +1 -1
  22. package/dist/bucket/index.workerd.js +7 -1
  23. package/dist/bucket/index.workerd.js.map +1 -1
  24. package/dist/cache/core/index.d.ts +26 -0
  25. package/dist/cache/core/index.d.ts.map +1 -1
  26. package/dist/cache/core/index.js +11 -1
  27. package/dist/cache/core/index.js.map +1 -1
  28. package/dist/cache/core/index.workerd.js +11 -1
  29. package/dist/cache/core/index.workerd.js.map +1 -1
  30. package/dist/cli/config/index.d.ts +7 -5
  31. package/dist/cli/config/index.d.ts.map +1 -1
  32. package/dist/cli/config/index.js +2 -3
  33. package/dist/cli/config/index.js.map +1 -1
  34. package/dist/cli/core/index.d.ts +420 -13
  35. package/dist/cli/core/index.d.ts.map +1 -1
  36. package/dist/cli/core/index.js +22 -511
  37. package/dist/cli/core/index.js.map +1 -1
  38. package/dist/cli/devtools/index.d.ts +4 -8
  39. package/dist/cli/devtools/index.d.ts.map +1 -1
  40. package/dist/cli/devtools/index.js +13 -15
  41. package/dist/cli/devtools/index.js.map +1 -1
  42. package/dist/cli/platform/index.d.ts +10 -13
  43. package/dist/cli/platform/index.d.ts.map +1 -1
  44. package/dist/cli/platform/index.js +18 -15
  45. package/dist/cli/platform/index.js.map +1 -1
  46. package/dist/cli/vendor/index.d.ts +10 -13
  47. package/dist/cli/vendor/index.d.ts.map +1 -1
  48. package/dist/cli/vendor/index.js +16 -13
  49. package/dist/cli/vendor/index.js.map +1 -1
  50. package/dist/core/index.browser.js +27 -3
  51. package/dist/core/index.browser.js.map +1 -1
  52. package/dist/core/index.d.ts +6 -3
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js +27 -3
  55. package/dist/core/index.js.map +1 -1
  56. package/dist/core/index.native.js +27 -3
  57. package/dist/core/index.native.js.map +1 -1
  58. package/dist/core/index.workerd.js +27 -3
  59. package/dist/core/index.workerd.js.map +1 -1
  60. package/dist/datetime/index.d.ts +69 -10
  61. package/dist/datetime/index.d.ts.map +1 -1
  62. package/dist/datetime/index.js +135 -13
  63. package/dist/datetime/index.js.map +1 -1
  64. package/dist/email/smtp/index.js +10636 -2
  65. package/dist/email/smtp/index.js.map +1 -1
  66. package/dist/fake/index.d.ts +8085 -4
  67. package/dist/fake/index.d.ts.map +1 -1
  68. package/dist/fake/index.js +33554 -3
  69. package/dist/fake/index.js.map +1 -1
  70. package/dist/lock/core/index.d.ts +30 -2
  71. package/dist/lock/core/index.d.ts.map +1 -1
  72. package/dist/lock/core/index.js +35 -12
  73. package/dist/lock/core/index.js.map +1 -1
  74. package/dist/mcp/index.d.ts +238 -31
  75. package/dist/mcp/index.d.ts.map +1 -1
  76. package/dist/mcp/index.js +198 -71
  77. package/dist/mcp/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +1 -1
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +4 -3
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +4877 -9
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +4 -3
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.d.ts +608 -1
  87. package/dist/orm/postgres/index.d.ts.map +1 -1
  88. package/dist/react/core/index.d.ts +102 -1
  89. package/dist/react/core/index.d.ts.map +1 -1
  90. package/dist/react/core/index.js +65 -1
  91. package/dist/react/core/index.js.map +1 -1
  92. package/dist/react/form/index.d.ts +6 -0
  93. package/dist/react/form/index.d.ts.map +1 -1
  94. package/dist/react/form/index.js +7 -7
  95. package/dist/react/form/index.js.map +1 -1
  96. package/dist/react/i18n/index.d.ts +7 -1
  97. package/dist/react/i18n/index.d.ts.map +1 -1
  98. package/dist/react/i18n/index.js +6 -0
  99. package/dist/react/i18n/index.js.map +1 -1
  100. package/dist/react/router/index.browser.js +20 -2
  101. package/dist/react/router/index.browser.js.map +1 -1
  102. package/dist/react/router/index.d.ts +36 -4
  103. package/dist/react/router/index.d.ts.map +1 -1
  104. package/dist/react/router/index.js +20 -2
  105. package/dist/react/router/index.js.map +1 -1
  106. package/dist/react/testing/chunk-6Ep1yQYe.js +16 -0
  107. package/dist/react/testing/index.d.ts +411 -1
  108. package/dist/react/testing/index.d.ts.map +1 -1
  109. package/dist/react/testing/index.js +12293 -13
  110. package/dist/react/testing/index.js.map +1 -1
  111. package/dist/react/ui/index.d.ts +195 -1
  112. package/dist/react/ui/index.d.ts.map +1 -1
  113. package/dist/react/ui/index.js +61 -1
  114. package/dist/react/ui/index.js.map +1 -1
  115. package/dist/scheduler/index.d.ts +84 -3
  116. package/dist/scheduler/index.d.ts.map +1 -1
  117. package/dist/scheduler/index.js +390 -1
  118. package/dist/scheduler/index.js.map +1 -1
  119. package/dist/scheduler/index.workerd.js +390 -1
  120. package/dist/scheduler/index.workerd.js.map +1 -1
  121. package/dist/security/index.d.ts +325 -2
  122. package/dist/security/index.d.ts.map +1 -1
  123. package/dist/security/index.js +1361 -2
  124. package/dist/security/index.js.map +1 -1
  125. package/dist/server/auth/index.d.ts +1054 -1
  126. package/dist/server/auth/index.d.ts.map +1 -1
  127. package/dist/server/auth/index.js +1223 -1
  128. package/dist/server/auth/index.js.map +1 -1
  129. package/dist/server/core/index.browser.js +10 -3
  130. package/dist/server/core/index.browser.js.map +1 -1
  131. package/dist/server/core/index.d.ts.map +1 -1
  132. package/dist/server/core/index.js +28 -5
  133. package/dist/server/core/index.js.map +1 -1
  134. package/dist/server/metrics/index.d.ts +514 -1
  135. package/dist/server/metrics/index.d.ts.map +1 -1
  136. package/dist/server/metrics/index.js +4374 -4
  137. package/dist/server/metrics/index.js.map +1 -1
  138. package/dist/server/swagger/index.d.ts.map +1 -1
  139. package/dist/server/swagger/index.js +3 -4
  140. package/dist/server/swagger/index.js.map +1 -1
  141. package/dist/websocket/index.browser.js +11 -5
  142. package/dist/websocket/index.browser.js.map +1 -1
  143. package/dist/websocket/index.d.ts +3 -1
  144. package/dist/websocket/index.d.ts.map +1 -1
  145. package/dist/websocket/index.js +21 -6
  146. package/dist/websocket/index.js.map +1 -1
  147. package/package.json +671 -263
  148. package/src/api/parameters/services/ParameterProvider.ts +21 -4
  149. package/src/api/users/__tests__/SessionService.spec.ts +99 -0
  150. package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
  151. package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
  152. package/src/api/users/entities/sessions.ts +6 -0
  153. package/src/api/users/jobs/UserJobs.ts +44 -17
  154. package/src/api/users/providers/RealmProvider.ts +4 -0
  155. package/src/api/users/services/SessionService.ts +27 -0
  156. package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
  157. package/src/bucket/index.ts +19 -2
  158. package/src/bucket/primitives/$bucket.ts +9 -1
  159. package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
  160. package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
  161. package/src/cache/core/index.ts +29 -0
  162. package/src/cache/core/primitives/$cache.ts +14 -1
  163. package/src/cli/config/defineConfig.ts +13 -15
  164. package/src/cli/core/__tests__/init.spec.ts +6 -7
  165. package/src/cli/core/services/ProjectScaffolder.ts +18 -14
  166. package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
  167. package/src/cli/core/templates/agentMd.ts +2 -10
  168. package/src/cli/core/templates/saasAdminLayoutTsx.ts +3 -3
  169. package/src/cli/devtools/index.ts +12 -26
  170. package/src/cli/platform/index.ts +15 -24
  171. package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
  172. package/src/cli/vendor/index.ts +14 -23
  173. package/src/core/Alepha.ts +11 -1
  174. package/src/core/helpers/ref.ts +18 -0
  175. package/src/core/index.shared.ts +1 -0
  176. package/src/core/providers/SchemaValidator.ts +9 -1
  177. package/src/core/providers/TypeProvider.ts +1 -2
  178. package/src/datetime/REFACTORING.md +118 -0
  179. package/src/datetime/providers/DateTimeProvider.ts +203 -24
  180. package/src/lock/core/index.ts +31 -0
  181. package/src/lock/core/primitives/$lock.ts +14 -1
  182. package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
  183. package/src/mcp/helpers/jsonrpc.ts +26 -1
  184. package/src/mcp/index.ts +10 -5
  185. package/src/mcp/interfaces/McpTypes.ts +83 -6
  186. package/src/mcp/primitives/$prompt.ts +18 -1
  187. package/src/mcp/primitives/$resource.ts +18 -1
  188. package/src/mcp/primitives/$tool.ts +83 -7
  189. package/src/mcp/providers/McpServerProvider.ts +74 -16
  190. package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
  191. package/src/orm/REFACTORING.md +330 -0
  192. package/src/orm/core/primitives/$transactional.ts +11 -0
  193. package/src/orm/core/schemas/updateSchema.ts +1 -1
  194. package/src/orm/core/services/PgRelationManager.ts +4 -2
  195. package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
  196. package/src/react/core/hooks/useQuery.ts +153 -0
  197. package/src/react/core/index.ts +1 -0
  198. package/src/react/form/services/FormModel.ts +15 -6
  199. package/src/react/form/services/parseField.ts +8 -0
  200. package/src/react/i18n/providers/I18nProvider.ts +8 -2
  201. package/src/react/router/__tests__/$page.spec.tsx +0 -16
  202. package/src/react/router/__tests__/ssr.spec.tsx +339 -0
  203. package/src/react/router/primitives/$page.ts +28 -4
  204. package/src/react/router/providers/ReactPageProvider.ts +27 -9
  205. package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
  206. package/src/react/ui/index.ts +6 -0
  207. package/src/react/ui/services/SchemaControl.ts +209 -0
  208. package/src/security/primitives/$issuer.ts +6 -3
  209. package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
  210. package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
  211. package/src/server/core/errors/ValidationError.ts +13 -1
  212. package/src/server/core/primitives/$action.ts +16 -5
  213. package/src/server/core/providers/ServerRouterProvider.ts +26 -4
  214. package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -7
  215. package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
  216. package/src/websocket/services/WebSocketClient.ts +11 -5
  217. package/src/mcp/transports/SseMcpTransport.ts +0 -182
@@ -1,6 +1,7 @@
1
1
  import { $inject, createPrimitive, KIND, Primitive } from "alepha";
2
2
  import type {
3
3
  McpContext,
4
+ McpIcon,
4
5
  McpResourceDescriptor,
5
6
  ResourceContent,
6
7
  ResourceHandler,
@@ -78,6 +79,17 @@ export interface ResourcePrimitiveOptions {
78
79
  */
79
80
  name?: string;
80
81
 
82
+ /**
83
+ * Human-friendly display title (spec 2025-11-25). Distinct from `name`,
84
+ * which remains the programmatic identifier.
85
+ */
86
+ title?: string;
87
+
88
+ /**
89
+ * Optional icons surfaced in client UIs (spec 2025-11-25 / SEP-973).
90
+ */
91
+ icons?: McpIcon[];
92
+
81
93
  /**
82
94
  * Description of what this resource contains.
83
95
  *
@@ -159,12 +171,17 @@ export class ResourcePrimitive extends Primitive<ResourcePrimitiveOptions> {
159
171
  * Convert the resource to an MCP resource descriptor for protocol messages.
160
172
  */
161
173
  public toDescriptor(): McpResourceDescriptor {
162
- return {
174
+ const descriptor: McpResourceDescriptor = {
163
175
  uri: this.uri,
164
176
  name: this.name,
165
177
  description: this.description,
166
178
  mimeType: this.mimeType,
167
179
  };
180
+ if (this.options.title) descriptor.title = this.options.title;
181
+ if (this.options.icons && this.options.icons.length > 0) {
182
+ descriptor.icons = this.options.icons;
183
+ }
184
+ return descriptor;
168
185
  }
169
186
  }
170
187
 
@@ -10,7 +10,9 @@ import {
10
10
  } from "alepha";
11
11
  import type {
12
12
  McpContext,
13
+ McpIcon,
13
14
  McpJsonSchema,
15
+ McpToolAnnotations,
14
16
  McpToolDescriptor,
15
17
  ToolHandlerArgs,
16
18
  ToolHandlerResult,
@@ -84,6 +86,15 @@ export interface ToolPrimitiveOptions<T extends ToolPrimitiveSchema> {
84
86
  */
85
87
  name?: string;
86
88
 
89
+ /**
90
+ * Human-friendly display title (spec 2025-11-25). Distinct from `name`,
91
+ * which remains the programmatic identifier. Clients use `title` in
92
+ * tool palettes / picker UIs.
93
+ *
94
+ * @example "Search Lore"
95
+ */
96
+ title?: string;
97
+
87
98
  /**
88
99
  * A human-readable description of what the tool does.
89
100
  *
@@ -95,6 +106,18 @@ export interface ToolPrimitiveOptions<T extends ToolPrimitiveSchema> {
95
106
  */
96
107
  description: string;
97
108
 
109
+ /**
110
+ * Behavior hints (spec 2025-03-26+). Clients use these to gate UI prompts
111
+ * (e.g. require confirmation before a tool with `destructiveHint: true`).
112
+ * None are guarantees — they are heuristics for the client, not the model.
113
+ */
114
+ annotations?: McpToolAnnotations;
115
+
116
+ /**
117
+ * Icons surfaced in client tool palettes / picker UIs (spec 2025-11-25).
118
+ */
119
+ icons?: McpIcon[];
120
+
98
121
  /**
99
122
  * TypeBox schema defining the tool's parameters and result type.
100
123
  *
@@ -141,6 +164,15 @@ export class ToolPrimitive<T extends ToolPrimitiveSchema> extends Primitive<
141
164
  return this.options.description;
142
165
  }
143
166
 
167
+ /**
168
+ * Whether the tool declared a result schema. When true, `tools/call`
169
+ * responses include `structuredContent` populated with the validated
170
+ * result (spec 2025-06-18).
171
+ */
172
+ public hasOutputSchema(): boolean {
173
+ return !!this.options.schema?.result;
174
+ }
175
+
144
176
  protected onInit(): void {
145
177
  this.mcpServer.registerTool(this);
146
178
  }
@@ -184,21 +216,57 @@ export class ToolPrimitive<T extends ToolPrimitiveSchema> extends Primitive<
184
216
 
185
217
  /**
186
218
  * Convert the tool to an MCP tool descriptor for protocol messages.
219
+ *
220
+ * Emits the spec 2025-11-25 surface: `title`, `annotations`, `icons`,
221
+ * and (when `schema.result` is defined) `outputSchema` so the server
222
+ * can populate `structuredContent` on call results.
187
223
  */
188
224
  public toDescriptor(): McpToolDescriptor {
189
- return {
225
+ const inputSchema: McpJsonSchema = this.options.schema?.params
226
+ ? this.schemaToJsonSchema(this.options.schema.params)
227
+ : { type: "object", properties: {}, required: [] };
228
+
229
+ const descriptor: McpToolDescriptor = {
190
230
  name: this.name,
191
231
  description: this.description,
192
- inputSchema: this.options.schema?.params
193
- ? this.schemaToJsonSchema(this.options.schema.params)
194
- : { type: "object", properties: {}, required: [] },
232
+ inputSchema,
195
233
  };
234
+
235
+ if (this.options.title) descriptor.title = this.options.title;
236
+ if (this.options.annotations)
237
+ descriptor.annotations = this.options.annotations;
238
+ if (this.options.icons && this.options.icons.length > 0) {
239
+ descriptor.icons = this.options.icons;
240
+ }
241
+
242
+ // Output schema is emitted when the tool declares `schema.result`,
243
+ // unlocking structured content on tools/call responses.
244
+ if (this.options.schema?.result) {
245
+ const out = this.propertyToJsonSchema(this.options.schema.result);
246
+ // The result schema may be a primitive — wrap so the descriptor
247
+ // value is always a JSON Schema object with `type`.
248
+ descriptor.outputSchema = (
249
+ typeof out === "object" && out !== null && "type" in out
250
+ ? out
251
+ : { type: "object", properties: {}, required: [] }
252
+ ) as McpJsonSchema;
253
+ }
254
+
255
+ return descriptor;
196
256
  }
197
257
 
198
258
  /**
199
259
  * Convert a TypeBox schema to JSON Schema format.
260
+ *
261
+ * Emits the 2020-12 dialect annotation at the root (spec 2025-11-25 /
262
+ * SEP-1613 — JSON Schema 2020-12 is the default dialect for MCP).
263
+ * The TypeBox shapes Alepha emits today are already 2020-12-compatible;
264
+ * this is just the dialect declaration.
200
265
  */
201
- protected schemaToJsonSchema(schema: TObject): McpJsonSchema {
266
+ protected schemaToJsonSchema(
267
+ schema: TObject,
268
+ options?: { root?: boolean },
269
+ ): McpJsonSchema {
202
270
  const properties: Record<string, unknown> = {};
203
271
  const required: string[] = [];
204
272
 
@@ -211,11 +279,19 @@ export class ToolPrimitive<T extends ToolPrimitiveSchema> extends Primitive<
211
279
  }
212
280
  }
213
281
 
214
- return {
282
+ const result: McpJsonSchema = {
215
283
  type: "object",
216
284
  properties,
217
285
  required,
218
286
  };
287
+
288
+ // Annotate the dialect on the root schema only (avoid noise on nested
289
+ // sub-schemas where MCP doesn't expect $schema).
290
+ if (options?.root !== false) {
291
+ result.$schema = "https://json-schema.org/draft/2020-12/schema";
292
+ }
293
+
294
+ return result;
219
295
  }
220
296
 
221
297
  /**
@@ -251,7 +327,7 @@ export class ToolPrimitive<T extends ToolPrimitiveSchema> extends Primitive<
251
327
  result.items = this.propertyToJsonSchema(schema.items as TSchema);
252
328
  }
253
329
  } else if (t.schema.isObject(schema)) {
254
- Object.assign(result, this.schemaToJsonSchema(schema));
330
+ Object.assign(result, this.schemaToJsonSchema(schema, { root: false }));
255
331
  } else if (t.schema.isUnsafe(schema) || t.schema.isOptional(schema)) {
256
332
  // Handle Unsafe types (like t.enum) and optional wrappers by checking the underlying type property
257
333
  const schemaAny = schema as { type?: string; enum?: unknown[] };
@@ -1,4 +1,4 @@
1
- import { $inject, Alepha } from "alepha";
1
+ import { $inject, Alepha, TypeBoxError } from "alepha";
2
2
  import { $logger } from "alepha/logger";
3
3
  import {
4
4
  McpError,
@@ -11,13 +11,14 @@ import {
11
11
  createErrorResponse,
12
12
  createInternalError,
13
13
  createResponse,
14
+ isSupportedProtocolVersion,
14
15
  MCP_PROTOCOL_VERSION,
16
+ SUPPORTED_PROTOCOL_VERSIONS,
15
17
  } from "../helpers/jsonrpc.ts";
16
18
  import type {
17
19
  JsonRpcRequest,
18
20
  JsonRpcResponse,
19
21
  McpCapabilities,
20
- McpContent,
21
22
  McpContext,
22
23
  McpInitializeResult,
23
24
  McpPromptDescriptor,
@@ -55,7 +56,20 @@ export class McpServerProvider {
55
56
 
56
57
  protected initialized = false;
57
58
 
58
- protected serverInfo: McpServerInfo = {
59
+ /**
60
+ * Protocol version negotiated with the client during `initialize`.
61
+ * Used by transports to validate the `MCP-Protocol-Version` header on
62
+ * subsequent HTTP requests (per spec 2025-06-18+).
63
+ */
64
+ public negotiatedVersion: string = MCP_PROTOCOL_VERSION;
65
+
66
+ /**
67
+ * Server identity returned during `initialize`. Consumers may override
68
+ * fields directly (e.g. `mcpServer.serverInfo = { name: "roadmap-mcp",
69
+ * version: "0.20.3", description: "..." }`) — the `description` field
70
+ * is supported per spec 2025-11-25 (minor change #2).
71
+ */
72
+ public serverInfo: McpServerInfo = {
59
73
  name: "alepha-mcp",
60
74
  version: "1.0.0",
61
75
  };
@@ -243,15 +257,25 @@ export class McpServerProvider {
243
257
  protected handleInitialize(
244
258
  params: Record<string, unknown>,
245
259
  ): McpInitializeResult {
260
+ const requested = params.protocolVersion;
261
+ // Echo the client's version when supported, otherwise reply with our
262
+ // preferred version (highest entry in SUPPORTED_PROTOCOL_VERSIONS).
263
+ // The client can then decide to retry, downgrade, or disconnect.
264
+ const negotiated = isSupportedProtocolVersion(requested)
265
+ ? requested
266
+ : SUPPORTED_PROTOCOL_VERSIONS[0];
267
+
246
268
  this.log.info("MCP client initializing", {
247
269
  clientInfo: params.clientInfo,
248
- protocolVersion: params.protocolVersion,
270
+ requestedProtocolVersion: requested,
271
+ negotiatedProtocolVersion: negotiated,
249
272
  });
250
273
 
251
274
  this.initialized = true;
275
+ this.negotiatedVersion = negotiated;
252
276
 
253
277
  return {
254
- protocolVersion: MCP_PROTOCOL_VERSION,
278
+ protocolVersion: negotiated,
255
279
  capabilities: this.getCapabilities(),
256
280
  serverInfo: this.serverInfo,
257
281
  };
@@ -276,24 +300,58 @@ export class McpServerProvider {
276
300
 
277
301
  const tool = this.tools.get(name);
278
302
  if (!tool) {
303
+ // McpToolNotFoundError is intentionally a JSON-RPC protocol error,
304
+ // not a tool execution error — see SEP-1303 (only validation/runtime
305
+ // failures of an existing tool are reported via isError: true).
279
306
  throw new McpToolNotFoundError(name);
280
307
  }
281
308
 
282
309
  try {
283
310
  const result = await tool.execute(args, context);
284
311
 
285
- const content: McpContent[] = [
286
- {
287
- type: "text",
288
- text:
289
- typeof result === "string"
290
- ? result
291
- : JSON.stringify(result ?? null),
292
- },
293
- ];
294
-
295
- return { content };
312
+ const callResult: McpToolCallResult = {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text:
317
+ typeof result === "string"
318
+ ? result
319
+ : JSON.stringify(result ?? null),
320
+ },
321
+ ],
322
+ };
323
+
324
+ // Spec 2025-06-18: when the tool declares an outputSchema, the server
325
+ // MUST populate `structuredContent` with the validated result. The
326
+ // text-stringified `content` block remains as a back-compat fallback.
327
+ if (tool.hasOutputSchema() && result !== undefined) {
328
+ callResult.structuredContent = result;
329
+ }
330
+
331
+ return callResult;
296
332
  } catch (error) {
333
+ // Spec 2025-11-25 / SEP-1303: input-validation failures (and other
334
+ // tool-runtime errors) are returned as Tool Execution Errors, not
335
+ // JSON-RPC protocol errors, so the model can self-correct.
336
+ // For TypeBox validation errors we surface the failing path so the
337
+ // model knows which argument was malformed.
338
+ if (error instanceof TypeBoxError) {
339
+ const path = error.value?.path || "/";
340
+ const message = error.value?.message || error.message;
341
+ return {
342
+ content: [
343
+ {
344
+ type: "text",
345
+ text: `Validation error at ${path}: ${message}`,
346
+ },
347
+ ],
348
+ structuredContent: {
349
+ errors: [{ path, message }],
350
+ },
351
+ isError: true,
352
+ };
353
+ }
354
+
297
355
  return {
298
356
  content: [
299
357
  {
@@ -0,0 +1,226 @@
1
+ import { $atom, $inject, $state, t } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import { $route } from "alepha/server";
4
+ import {
5
+ createErrorResponse,
6
+ createParseError,
7
+ JsonRpcParseError,
8
+ parseMessage,
9
+ } from "../helpers/jsonrpc.ts";
10
+ import type { McpContext } from "../interfaces/McpTypes.ts";
11
+ import { McpServerProvider } from "../providers/McpServerProvider.ts";
12
+
13
+ // ---------------------------------------------------------------------------------------------------------------------
14
+
15
+ export const mcpStreamableHttpOptions = $atom({
16
+ name: "alepha.mcp.streamableHttp.options",
17
+ description: "Configuration options for the MCP Streamable HTTP transport.",
18
+ schema: t.object({
19
+ /**
20
+ * Path for the MCP endpoint. Single endpoint for both requests and
21
+ * (optional) server-streamed responses, per spec 2025-03-26+.
22
+ */
23
+ path: t.text({ default: "/mcp" }),
24
+ /**
25
+ * Allow-list of `Origin` header values accepted on incoming requests.
26
+ * Empty array (default) means "allow any". When set, browser-originated
27
+ * requests with a non-matching `Origin` are rejected with 403 Forbidden,
28
+ * blocking DNS-rebinding attacks against localhost MCP servers.
29
+ *
30
+ * Server-to-server callers (no `Origin` header) are always allowed.
31
+ *
32
+ * Spec 2025-11-25, PR #1439.
33
+ */
34
+ allowedOrigins: t.array(t.text(), { default: [] }),
35
+ }),
36
+ default: {
37
+ path: "/mcp",
38
+ allowedOrigins: [],
39
+ },
40
+ });
41
+
42
+ // Backward-compat alias for the legacy atom name. Prefer
43
+ // `mcpStreamableHttpOptions` going forward; this re-export keeps existing
44
+ // consumer imports compiling and will be removed once they migrate.
45
+ export const mcpSseOptions = mcpStreamableHttpOptions;
46
+
47
+ // ---------------------------------------------------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Streamable HTTP transport for MCP communication.
51
+ *
52
+ * Implements the 2025-03-26+ Streamable HTTP transport: a single `/mcp`
53
+ * endpoint that accepts JSON-RPC over POST and returns either
54
+ * `application/json` (single response, the default) or
55
+ * `text/event-stream` (when the server wants to stream multiple messages).
56
+ *
57
+ * Designed for serverless deployment (Cloudflare Workers, etc.) — there is
58
+ * no long-lived GET stream. GET on the endpoint returns 405 Method Not
59
+ * Allowed; clients that want server-initiated push must rely on the POST
60
+ * response stream when the server upgrades to SSE for that particular call.
61
+ *
62
+ * Spec compliance:
63
+ * - 2025-06-18: validates `MCP-Protocol-Version` header on every request
64
+ * after `initialize` against the version negotiated and stored on
65
+ * `McpServerProvider`.
66
+ * - 2025-11-25: rejects requests with a non-allow-listed `Origin` header
67
+ * (PR #1439). See {@link mcpStreamableHttpOptions.allowedOrigins}.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { Alepha, run } from "alepha";
72
+ * import { AlephaServer } from "alepha/server";
73
+ * import { AlephaMcp, StreamableHttpMcpTransport } from "alepha/mcp";
74
+ *
75
+ * class MyTools {
76
+ * // ... tool definitions
77
+ * }
78
+ *
79
+ * run(
80
+ * Alepha.create()
81
+ * .with(AlephaServer)
82
+ * .with(AlephaMcp)
83
+ * .with(StreamableHttpMcpTransport)
84
+ * .with(MyTools)
85
+ * );
86
+ * ```
87
+ */
88
+ export class StreamableHttpMcpTransport {
89
+ protected readonly log = $logger();
90
+ protected readonly options = $state(mcpStreamableHttpOptions);
91
+ protected readonly mcpServer = $inject(McpServerProvider);
92
+
93
+ /**
94
+ * GET on the MCP endpoint is not supported in this transport. Returning
95
+ * 405 (rather than serving the legacy two-endpoint SSE pattern) is the
96
+ * spec-allowed response for servers that don't offer server-initiated
97
+ * push outside of an active POST.
98
+ */
99
+ notAllowed = $route({
100
+ method: "GET",
101
+ path: this.options.path,
102
+ handler: (request) => {
103
+ request.reply.status = 405;
104
+ request.reply.headers.allow = "POST";
105
+ request.reply.headers["content-type"] = "application/json";
106
+ request.reply.body = JSON.stringify({
107
+ error: "Method Not Allowed. Use POST for MCP messages.",
108
+ });
109
+ },
110
+ });
111
+
112
+ /**
113
+ * POST endpoint for client-to-server JSON-RPC messages.
114
+ * Returns `application/json` for single responses; tools that need to
115
+ * stream progress would upgrade to `text/event-stream` (deferred until a
116
+ * concrete need exists).
117
+ */
118
+ message = $route({
119
+ method: "POST",
120
+ path: this.options.path,
121
+ schema: {
122
+ body: t.json(),
123
+ },
124
+ handler: async (request) => {
125
+ try {
126
+ // Origin allow-list check (spec 2025-11-25 / PR #1439).
127
+ const originRaw = request.headers.origin;
128
+ const origin = Array.isArray(originRaw) ? originRaw[0] : originRaw;
129
+ if (
130
+ origin &&
131
+ this.options.allowedOrigins.length > 0 &&
132
+ !this.options.allowedOrigins.includes(origin)
133
+ ) {
134
+ this.log.warn("Rejected MCP request with non-allowed Origin", {
135
+ origin,
136
+ allowed: this.options.allowedOrigins,
137
+ });
138
+ request.reply.status = 403;
139
+ request.reply.headers["content-type"] = "application/json";
140
+ request.reply.body = JSON.stringify({
141
+ error: "Forbidden: Origin not allowed",
142
+ });
143
+ return;
144
+ }
145
+
146
+ const body =
147
+ typeof request.body === "string"
148
+ ? request.body
149
+ : JSON.stringify(request.body);
150
+
151
+ this.log.debug("MCP request body", {
152
+ body,
153
+ bodyType: typeof request.body,
154
+ });
155
+
156
+ const rpcRequest = parseMessage(body);
157
+
158
+ // Build context from request headers
159
+ const headers = { ...request.headers } as Record<
160
+ string,
161
+ string | string[] | undefined
162
+ >;
163
+
164
+ // Spec 2025-06-18+: every HTTP request after `initialize` MUST carry
165
+ // an `MCP-Protocol-Version` header matching the negotiated version.
166
+ // Reject mismatches with 400 so the client doesn't silently drift.
167
+ if (rpcRequest.method !== "initialize") {
168
+ const headerRaw = headers["mcp-protocol-version"];
169
+ const headerVersion = Array.isArray(headerRaw)
170
+ ? headerRaw[0]
171
+ : headerRaw;
172
+ if (
173
+ headerVersion &&
174
+ headerVersion !== this.mcpServer.negotiatedVersion
175
+ ) {
176
+ this.log.warn("MCP-Protocol-Version header mismatch", {
177
+ header: headerVersion,
178
+ negotiated: this.mcpServer.negotiatedVersion,
179
+ });
180
+ request.reply.status = 400;
181
+ request.reply.headers["content-type"] = "application/json";
182
+ request.reply.body = JSON.stringify({
183
+ error: `MCP-Protocol-Version mismatch: expected ${this.mcpServer.negotiatedVersion}, got ${headerVersion}`,
184
+ });
185
+ return;
186
+ }
187
+ }
188
+
189
+ const context: McpContext = { headers };
190
+
191
+ const response = await this.mcpServer.handleMessage(
192
+ rpcRequest,
193
+ context,
194
+ );
195
+
196
+ if (response) {
197
+ request.reply.headers["content-type"] = "application/json";
198
+ request.reply.body = JSON.stringify(response);
199
+ } else {
200
+ request.reply.status = 204;
201
+ }
202
+ } catch (error) {
203
+ if (error instanceof JsonRpcParseError) {
204
+ request.reply.status = 400;
205
+ request.reply.headers["content-type"] = "application/json";
206
+ request.reply.body = JSON.stringify(
207
+ createErrorResponse(0, createParseError(error.message)),
208
+ );
209
+ } else {
210
+ this.log.error("Failed to process MCP message", error);
211
+ request.reply.status = 500;
212
+ request.reply.body = JSON.stringify({
213
+ error: (error as Error).message,
214
+ });
215
+ }
216
+ }
217
+ },
218
+ });
219
+ }
220
+
221
+ /**
222
+ * @deprecated Use {@link StreamableHttpMcpTransport}. The 2024-11-05
223
+ * two-endpoint HTTP+SSE pattern was replaced by Streamable HTTP in spec
224
+ * 2025-03-26. This alias is preserved for one release to ease migration.
225
+ */
226
+ export const SseMcpTransport = StreamableHttpMcpTransport;