@ampless/runtime 1.0.0-beta.73 → 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 };
@@ -207,7 +207,7 @@ function createMarkdownRouteHandler(ampless) {
207
207
  }
208
208
 
209
209
  // src/routes/llms.ts
210
- import { collectBounded } from "ampless";
210
+ import { collectBounded, resolvePublicMcpEndpoint } from "ampless";
211
211
  var DEFAULT_LIMIT = 100;
212
212
  var MIN_LIMIT = 1;
213
213
  var MAX_LIMIT = 1e3;
@@ -290,6 +290,13 @@ function createLlmsTxtRouteHandler(ampless) {
290
290
  }
291
291
  const note = truncationNote(truncated, limit);
292
292
  if (note) blocks.push(note);
293
+ if (ampless.cmsConfig.ai?.publicMcp === true) {
294
+ const resolvedMcpUrl = resolvePublicMcpEndpoint(true, settings.site.url);
295
+ const mcpUrl = typeof resolvedMcpUrl === "string" ? resolvedMcpUrl : "/api/mcp";
296
+ blocks.push(
297
+ `This site also exposes a read-only MCP endpoint at ${mcpUrl} (JSON-RPC over HTTP POST; tools: list_posts, get_post, search_posts, list_tags; published posts only).`
298
+ );
299
+ }
293
300
  if (items.length > 0) {
294
301
  const lines = items.map((post) => {
295
302
  const encodedSlug = fixedEncodeURIComponent(post.slug);
@@ -337,6 +344,42 @@ import {
337
344
  MAX_BATCH
338
345
  } from "@ampless/mcp-server/jsonrpc";
339
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
340
383
  var MAX_BODY_BYTES = 64 * 1024;
341
384
  var RATE_WINDOW_MS = 6e4;
342
385
  var RATE_MAX = 600;
@@ -356,28 +399,23 @@ function retryAfterSeconds() {
356
399
  function _resetPublicMcpRateLimit() {
357
400
  rateState = { count: 0, windowExpires: 0 };
358
401
  }
359
- function corsHeaders(extra = {}) {
360
- return {
361
- "Access-Control-Allow-Origin": "*",
362
- "Access-Control-Allow-Methods": "POST, OPTIONS",
363
- "Access-Control-Allow-Headers": "content-type, mcp-protocol-version",
364
- "Access-Control-Max-Age": "86400",
365
- ...extra
366
- };
402
+ function corsHeaders2(extra = {}) {
403
+ return corsHeaders("POST, OPTIONS", extra);
367
404
  }
405
+ var STATIC_SERVER_INFO = { name: "ampless-mcp", version: "0.2" };
368
406
  function jsonResponse(status, body) {
369
407
  return new Response(JSON.stringify(body), {
370
408
  status,
371
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
409
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
372
410
  });
373
411
  }
374
412
  function notFound() {
375
- return new Response("Not Found", { status: 404, headers: corsHeaders() });
413
+ return new Response("Not Found", { status: 404, headers: corsHeaders2() });
376
414
  }
377
415
  function rateLimited() {
378
416
  return new Response(JSON.stringify(jsonRpcError(null, -32e3, "Rate limit exceeded")), {
379
417
  status: 429,
380
- headers: corsHeaders({
418
+ headers: corsHeaders2({
381
419
  "Content-Type": "application/json",
382
420
  "Cache-Control": "no-store",
383
421
  "Retry-After": String(retryAfterSeconds())
@@ -389,7 +427,7 @@ function payloadTooLarge() {
389
427
  JSON.stringify(jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Payload too large")),
390
428
  {
391
429
  status: 413,
392
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
430
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
393
431
  }
394
432
  );
395
433
  }
@@ -432,6 +470,11 @@ function createPublicMcpRouteHandler(ampless) {
432
470
  function gateOpen() {
433
471
  return ampless.cmsConfig.ai?.publicMcp === true;
434
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
+ }
435
478
  async function POST(request) {
436
479
  try {
437
480
  if (!gateOpen()) return notFound();
@@ -460,7 +503,7 @@ function createPublicMcpRouteHandler(ampless) {
460
503
  const result = await dispatchJsonRpcMessage(parsed, {
461
504
  tools: publicTools,
462
505
  getContext: () => publicCtx,
463
- serverInfo: { name: "ampless-mcp", version: "0.2" },
506
+ serverInfo: await resolveServerInfo(),
464
507
  formatToolError,
465
508
  maxBatch: MAX_BATCH
466
509
  });
@@ -470,7 +513,7 @@ function createPublicMcpRouteHandler(ampless) {
470
513
  if (result.status === "no-content") {
471
514
  return new Response(null, {
472
515
  status: 202,
473
- headers: corsHeaders({ "Cache-Control": "no-store" })
516
+ headers: corsHeaders2({ "Cache-Control": "no-store" })
474
517
  });
475
518
  }
476
519
  return jsonResponse(200, result.body);
@@ -481,15 +524,128 @@ function createPublicMcpRouteHandler(ampless) {
481
524
  }
482
525
  async function OPTIONS(_request) {
483
526
  if (!gateOpen()) return notFound();
484
- return new Response(null, { status: 204, headers: corsHeaders() });
527
+ return new Response(null, { status: 204, headers: corsHeaders2() });
485
528
  }
486
529
  return { POST, OPTIONS };
487
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
+ }
488
643
  export {
489
644
  _resetPublicMcpRateLimit,
490
645
  createFeedRouteHandler,
491
646
  createLlmsTxtRouteHandler,
492
647
  createMarkdownRouteHandler,
648
+ createMcpDiscoveryRouteHandlers,
493
649
  createOgRouteHandler,
494
650
  createPublicMcpRouteHandler,
495
651
  createRawRouteHandler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-beta.73",
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.67",
55
- "ampless": "1.0.0-beta.60",
56
- "@ampless/plugin-og-image": "0.2.0-beta.60"
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"