@ampless/runtime 1.0.0-beta.74 → 1.0.0-beta.75

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.
@@ -144,7 +144,13 @@ var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
144
144
  // internal rewrite targets — direct hits should not be middleware-driven
145
145
  "raw",
146
146
  "static",
147
- "md"
147
+ "md",
148
+ // `/.well-known/*` is passed through to Next (RFC 8615 registry paths):
149
+ // the exact `/.well-known/mcp/catalog.json` is rewritten below when MCP
150
+ // discovery is on; everything else (e.g. a future
151
+ // `/.well-known/mcp-registry-auth` an operator drops in) is served by
152
+ // Next without a wasted AppSync flag fetch or a middleware 404.
153
+ ".well-known"
148
154
  ]);
149
155
  function createAmplessMiddleware(opts) {
150
156
  return async function middleware(request) {
@@ -160,6 +166,10 @@ function createAmplessMiddleware(opts) {
160
166
  }
161
167
  requestHeaders.set(AMPLESS_PATHNAME_HEADER, url.pathname);
162
168
  const passthrough = () => NextResponse.next({ request: { headers: requestHeaders } });
169
+ if (url.pathname === "/.well-known/mcp/catalog.json" && opts.cmsConfig.ai?.publicMcp === true && opts.cmsConfig.ai?.mcpDiscovery === true) {
170
+ url.pathname = "/api/mcp/catalog.json";
171
+ return NextResponse.rewrite(url, { request: { headers: requestHeaders } });
172
+ }
163
173
  const segments = url.pathname.split("/").filter(Boolean);
164
174
  if (segments.length === 0) {
165
175
  return passthrough();
@@ -148,4 +148,21 @@ declare function _resetPublicMcpRateLimit(): void;
148
148
  */
149
149
  declare function createPublicMcpRouteHandler(ampless: Ampless): PublicMcpRouteHandlers;
150
150
 
151
- export { type FeedRouteHandler, type LlmsTxtRouteHandler, type MarkdownRouteHandler, type OgRouteHandler, type PublicMcpRouteHandlers, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, _resetPublicMcpRateLimit, createFeedRouteHandler, createLlmsTxtRouteHandler, createMarkdownRouteHandler, createOgRouteHandler, createPublicMcpRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
151
+ interface McpDiscoveryRouteHandlers {
152
+ catalog: {
153
+ GET: (request: Request) => Promise<Response>;
154
+ OPTIONS: (request: Request) => Promise<Response>;
155
+ };
156
+ serverCard: {
157
+ GET: (request: Request) => Promise<Response>;
158
+ OPTIONS: (request: Request) => Promise<Response>;
159
+ };
160
+ }
161
+ /**
162
+ * Build the discovery handlers. The template mounts the catalog handlers
163
+ * at `app/api/mcp/catalog.json/route.ts` (middleware rewrite target) and
164
+ * the server-card handlers at `app/api/mcp/server-card/route.ts`.
165
+ */
166
+ declare function createMcpDiscoveryRouteHandlers(ampless: Ampless): McpDiscoveryRouteHandlers;
167
+
168
+ export { type FeedRouteHandler, type LlmsTxtRouteHandler, type MarkdownRouteHandler, type McpDiscoveryRouteHandlers, type OgRouteHandler, type PublicMcpRouteHandlers, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, _resetPublicMcpRateLimit, createFeedRouteHandler, createLlmsTxtRouteHandler, createMarkdownRouteHandler, createMcpDiscoveryRouteHandlers, createOgRouteHandler, createPublicMcpRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
@@ -344,6 +344,42 @@ import {
344
344
  MAX_BATCH
345
345
  } from "@ampless/mcp-server/jsonrpc";
346
346
  import { publicTools } from "@ampless/mcp-server/public";
347
+
348
+ // src/routes/_mcp-shared.ts
349
+ var PUBLIC_MCP_SERVER_VERSION = "0.2.0";
350
+ function corsHeaders(methods, extra = {}) {
351
+ return {
352
+ "Access-Control-Allow-Origin": "*",
353
+ "Access-Control-Allow-Methods": methods,
354
+ "Access-Control-Allow-Headers": "content-type, mcp-protocol-version",
355
+ "Access-Control-Max-Age": "86400",
356
+ ...extra
357
+ };
358
+ }
359
+ var CARD_NAME_PATTERN = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
360
+ var CARD_NAME_MIN = 3;
361
+ var CARD_NAME_MAX = 200;
362
+ var SERVER_NAME_SUFFIX = "ampless-mcp";
363
+ function resolvePublicMcpIdentity(siteUrl) {
364
+ if (!siteUrl?.trim()) return null;
365
+ let hostname;
366
+ try {
367
+ const parsed = new URL(siteUrl);
368
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
369
+ hostname = parsed.hostname;
370
+ } catch {
371
+ return null;
372
+ }
373
+ const labels = hostname.split(".").filter(Boolean);
374
+ if (labels.length === 0) return null;
375
+ const namespace = labels.reverse().join(".");
376
+ const name = `${namespace}/${SERVER_NAME_SUFFIX}`;
377
+ if (name.length < CARD_NAME_MIN || name.length > CARD_NAME_MAX) return null;
378
+ if (!CARD_NAME_PATTERN.test(name)) return null;
379
+ return { name, version: PUBLIC_MCP_SERVER_VERSION };
380
+ }
381
+
382
+ // src/routes/public-mcp.ts
347
383
  var MAX_BODY_BYTES = 64 * 1024;
348
384
  var RATE_WINDOW_MS = 6e4;
349
385
  var RATE_MAX = 600;
@@ -363,28 +399,23 @@ function retryAfterSeconds() {
363
399
  function _resetPublicMcpRateLimit() {
364
400
  rateState = { count: 0, windowExpires: 0 };
365
401
  }
366
- function corsHeaders(extra = {}) {
367
- return {
368
- "Access-Control-Allow-Origin": "*",
369
- "Access-Control-Allow-Methods": "POST, OPTIONS",
370
- "Access-Control-Allow-Headers": "content-type, mcp-protocol-version",
371
- "Access-Control-Max-Age": "86400",
372
- ...extra
373
- };
402
+ function corsHeaders2(extra = {}) {
403
+ return corsHeaders("POST, OPTIONS", extra);
374
404
  }
405
+ var STATIC_SERVER_INFO = { name: "ampless-mcp", version: "0.2" };
375
406
  function jsonResponse(status, body) {
376
407
  return new Response(JSON.stringify(body), {
377
408
  status,
378
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
409
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
379
410
  });
380
411
  }
381
412
  function notFound() {
382
- return new Response("Not Found", { status: 404, headers: corsHeaders() });
413
+ return new Response("Not Found", { status: 404, headers: corsHeaders2() });
383
414
  }
384
415
  function rateLimited() {
385
416
  return new Response(JSON.stringify(jsonRpcError(null, -32e3, "Rate limit exceeded")), {
386
417
  status: 429,
387
- headers: corsHeaders({
418
+ headers: corsHeaders2({
388
419
  "Content-Type": "application/json",
389
420
  "Cache-Control": "no-store",
390
421
  "Retry-After": String(retryAfterSeconds())
@@ -396,7 +427,7 @@ function payloadTooLarge() {
396
427
  JSON.stringify(jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Payload too large")),
397
428
  {
398
429
  status: 413,
399
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
430
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
400
431
  }
401
432
  );
402
433
  }
@@ -439,6 +470,11 @@ function createPublicMcpRouteHandler(ampless) {
439
470
  function gateOpen() {
440
471
  return ampless.cmsConfig.ai?.publicMcp === true;
441
472
  }
473
+ async function resolveServerInfo() {
474
+ if (ampless.cmsConfig.ai?.mcpDiscovery !== true) return STATIC_SERVER_INFO;
475
+ const settings = await ampless.loadSiteSettings();
476
+ return resolvePublicMcpIdentity(settings.site.url) ?? STATIC_SERVER_INFO;
477
+ }
442
478
  async function POST(request) {
443
479
  try {
444
480
  if (!gateOpen()) return notFound();
@@ -467,7 +503,7 @@ function createPublicMcpRouteHandler(ampless) {
467
503
  const result = await dispatchJsonRpcMessage(parsed, {
468
504
  tools: publicTools,
469
505
  getContext: () => publicCtx,
470
- serverInfo: { name: "ampless-mcp", version: "0.2" },
506
+ serverInfo: await resolveServerInfo(),
471
507
  formatToolError,
472
508
  maxBatch: MAX_BATCH
473
509
  });
@@ -477,7 +513,7 @@ function createPublicMcpRouteHandler(ampless) {
477
513
  if (result.status === "no-content") {
478
514
  return new Response(null, {
479
515
  status: 202,
480
- headers: corsHeaders({ "Cache-Control": "no-store" })
516
+ headers: corsHeaders2({ "Cache-Control": "no-store" })
481
517
  });
482
518
  }
483
519
  return jsonResponse(200, result.body);
@@ -488,15 +524,128 @@ function createPublicMcpRouteHandler(ampless) {
488
524
  }
489
525
  async function OPTIONS(_request) {
490
526
  if (!gateOpen()) return notFound();
491
- return new Response(null, { status: 204, headers: corsHeaders() });
527
+ return new Response(null, { status: 204, headers: corsHeaders2() });
492
528
  }
493
529
  return { POST, OPTIONS };
494
530
  }
531
+
532
+ // src/routes/mcp-discovery.ts
533
+ import { resolvePublicMcpEndpoint as resolvePublicMcpEndpoint2 } from "ampless";
534
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@ampless/mcp-server/jsonrpc";
535
+ var CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
536
+ var CARD_CONTENT_TYPE = "application/mcp-server-card+json";
537
+ var CATALOG_ENTRY_TYPE = "application/mcp-server-card+json";
538
+ var CARD_TEXT_MAX = 100;
539
+ var DESCRIPTION_TEXT = "read-only MCP endpoint for published posts (list, get, search, tags).";
540
+ var CONTROL_CHARS_RE2 = /[\x00-\x1f\x7f]/g;
541
+ function normalizeText2(s) {
542
+ return s.replace(CONTROL_CHARS_RE2, " ").replace(/\s+/g, " ").trim();
543
+ }
544
+ function truncate(s, max) {
545
+ const codePoints = [...s];
546
+ return codePoints.length > max ? codePoints.slice(0, max).join("") : s;
547
+ }
548
+ function discoveryEnabled(ampless) {
549
+ return ampless.cmsConfig.ai?.publicMcp === true && ampless.cmsConfig.ai?.mcpDiscovery === true;
550
+ }
551
+ async function resolveContext(ampless) {
552
+ const settings = await ampless.loadSiteSettings();
553
+ const endpoint = resolvePublicMcpEndpoint2(true, settings.site.url);
554
+ if (typeof endpoint !== "string") return null;
555
+ const identity = resolvePublicMcpIdentity(settings.site.url);
556
+ if (!identity) return null;
557
+ const url = new URL(endpoint);
558
+ const { origin, hostname } = url;
559
+ return {
560
+ origin,
561
+ hostname,
562
+ endpoint: `${origin}/api/mcp`,
563
+ identity,
564
+ siteName: settings.site.name ?? ""
565
+ };
566
+ }
567
+ function notFound2() {
568
+ return new Response("Not Found", {
569
+ status: 404,
570
+ headers: corsHeaders("GET, OPTIONS")
571
+ });
572
+ }
573
+ function jsonResponse2(body, contentType) {
574
+ return new Response(JSON.stringify(body), {
575
+ status: 200,
576
+ headers: corsHeaders("GET, OPTIONS", {
577
+ "Content-Type": contentType,
578
+ // Spec SHOULD value — discovery documents are cheap and stable.
579
+ "Cache-Control": "public, max-age=3600"
580
+ })
581
+ });
582
+ }
583
+ function createMcpDiscoveryRouteHandlers(ampless) {
584
+ async function catalogGET(_request) {
585
+ if (!discoveryEnabled(ampless)) return notFound2();
586
+ const ctx = await resolveContext(ampless);
587
+ if (!ctx) {
588
+ console.warn(
589
+ "[ampless] MCP discovery catalog: site.url is unset or unresolvable \u2014 skipping (404)"
590
+ );
591
+ return notFound2();
592
+ }
593
+ const body = {
594
+ specVersion: "draft",
595
+ entries: [
596
+ {
597
+ // URN tail matches the Card name's `/`-suffix (`ampless-mcp`).
598
+ identifier: `urn:air:${ctx.hostname}:ampless-mcp`,
599
+ type: CATALOG_ENTRY_TYPE,
600
+ url: `${ctx.origin}/api/mcp/server-card`
601
+ }
602
+ ]
603
+ };
604
+ return jsonResponse2(body, "application/json");
605
+ }
606
+ async function serverCardGET(_request) {
607
+ if (!discoveryEnabled(ampless)) return notFound2();
608
+ const ctx = await resolveContext(ampless);
609
+ if (!ctx) {
610
+ console.warn(
611
+ "[ampless] MCP discovery server-card: site.url is unset or unresolvable \u2014 skipping (404)"
612
+ );
613
+ return notFound2();
614
+ }
615
+ const normalizedName = normalizeText2(ctx.siteName);
616
+ const rawDescription = normalizedName ? `${normalizedName} \u2014 ${DESCRIPTION_TEXT}` : DESCRIPTION_TEXT;
617
+ const card = {
618
+ $schema: CARD_SCHEMA_URL,
619
+ name: ctx.identity.name,
620
+ version: ctx.identity.version,
621
+ description: truncate(rawDescription, CARD_TEXT_MAX)
622
+ };
623
+ if (normalizedName) card.title = truncate(normalizedName, CARD_TEXT_MAX);
624
+ card.websiteUrl = ctx.origin;
625
+ card.remotes = [
626
+ {
627
+ type: "streamable-http",
628
+ url: ctx.endpoint,
629
+ supportedProtocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS]
630
+ }
631
+ ];
632
+ return jsonResponse2(card, CARD_CONTENT_TYPE);
633
+ }
634
+ async function optionsHandler(_request) {
635
+ if (!discoveryEnabled(ampless)) return notFound2();
636
+ return new Response(null, { status: 204, headers: corsHeaders("GET, OPTIONS") });
637
+ }
638
+ return {
639
+ catalog: { GET: catalogGET, OPTIONS: optionsHandler },
640
+ serverCard: { GET: serverCardGET, OPTIONS: optionsHandler }
641
+ };
642
+ }
495
643
  export {
496
644
  _resetPublicMcpRateLimit,
497
645
  createFeedRouteHandler,
498
646
  createLlmsTxtRouteHandler,
499
647
  createMarkdownRouteHandler,
648
+ createMcpDiscoveryRouteHandlers,
500
649
  createOgRouteHandler,
501
650
  createPublicMcpRouteHandler,
502
651
  createRawRouteHandler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-beta.74",
3
+ "version": "1.0.0-beta.75",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,9 +51,9 @@
51
51
  "marked": "^18.0.4",
52
52
  "sanitize-html": "^2.17.4",
53
53
  "tailwind-merge": "^3.6.0",
54
- "@ampless/mcp-server": "1.0.0-beta.68",
55
- "ampless": "1.0.0-beta.61",
56
- "@ampless/plugin-og-image": "0.2.0-beta.61"
54
+ "@ampless/mcp-server": "1.0.0-beta.69",
55
+ "@ampless/plugin-og-image": "0.2.0-beta.62",
56
+ "ampless": "1.0.0-beta.62"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@aws-amplify/adapter-nextjs": "^1",
@@ -66,6 +66,8 @@
66
66
  "@types/react": "^19.2.15",
67
67
  "@types/react-dom": "^19.2.3",
68
68
  "@types/sanitize-html": "^2.16.1",
69
+ "ajv": "^8.17.1",
70
+ "ajv-formats": "^3.0.1",
69
71
  "next": "^16.2.6",
70
72
  "react": "^19.2.6",
71
73
  "react-dom": "^19.2.6"