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

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.
package/README.ja.md CHANGED
@@ -121,7 +121,7 @@ CORS(匿名・読み取り専用)、64KB のボディ上限、粗い warm-in
121
121
  また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
122
122
  `cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
123
123
  `Cache-Control` を算出してレスポンスに付与します。キャッシュ戦略の
124
- 契約については `docs/CONTENT.md` を参照してください。
124
+ 契約については `https://github.com/heavymoons/ampless/wiki/CONTENT` を参照してください。
125
125
 
126
126
  ## サブパス
127
127
 
package/README.md CHANGED
@@ -119,7 +119,7 @@ returns `{ POST, OPTIONS }`; the template destructures both (a single
119
119
  It also computes `Cache-Control` from `post.metadata.cache` (auto /
120
120
  deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
121
121
  freshTtlSeconds, deepTtlSeconds}` and sets the header on the response.
122
- See `docs/CONTENT.md` for the cache strategy contract.
122
+ See `https://github.com/heavymoons/ampless/wiki/CONTENT` for the cache strategy contract.
123
123
 
124
124
  ## Sub-paths
125
125
 
@@ -1,6 +1,5 @@
1
1
  // src/stream-s3.ts
2
2
  import { getProperties } from "aws-amplify/storage/server";
3
- import { cookies } from "next/headers";
4
3
  var DEFAULT_THRESHOLD_BYTES = 6 * 1024 * 1024;
5
4
  var META_CACHE_MAX = 1e3;
6
5
  var META_CACHE_TTL_MS = 5 * 6e4;
@@ -111,7 +110,7 @@ async function streamS3Object(amplifyServerContext, key, options) {
111
110
  }
112
111
  async function streamS3ObjectWithRunner(runWithAmplifyServerContext, key, options) {
113
112
  return runWithAmplifyServerContext({
114
- nextServerContext: { cookies },
113
+ nextServerContext: null,
115
114
  operation: (ctx) => streamS3Object(ctx, key, options)
116
115
  });
117
116
  }
@@ -3,7 +3,6 @@ import { Metadata } from 'next';
3
3
  import { Ampless } from '../index.js';
4
4
  import 'ampless';
5
5
  import 'aws-amplify/storage/server';
6
- import 'next/headers';
7
6
 
8
7
  interface Props$2 {
9
8
  params: Promise<Record<string, never>>;
package/dist/index.d.ts CHANGED
@@ -3,7 +3,6 @@ export { Config, Post, ThemeManifest } from 'ampless';
3
3
  import { Metadata } from 'next';
4
4
  import { ReactNode } from 'react';
5
5
  import { getProperties } from 'aws-amplify/storage/server';
6
- import { cookies } from 'next/headers';
7
6
 
8
7
  interface StorageOutput {
9
8
  bucket_name: string;
@@ -625,16 +624,17 @@ declare function _resetStreamS3Cache(): void;
625
624
  */
626
625
  declare function streamS3Object(amplifyServerContext: AmplifyServerContext, key: string, options: StreamS3Options): Promise<Response>;
627
626
  /**
628
- * Convenience wrapper that pulls in the standard Amplify SSR cookie
629
- * adapter so route handlers don't have to duplicate the
627
+ * Convenience wrapper so route handlers don't have to duplicate the
630
628
  * `runWithAmplifyServerContext` plumbing. Pass the runner from
631
629
  * `createServerRunner({ config: outputs })` and the rest is the same
632
630
  * shape as `streamS3Object`.
631
+ *
632
+ * Runs with `nextServerContext: null` (Amplify's stateless guest path):
633
+ * no cookie read or write, so the public-cacheable response can never
634
+ * carry a Set-Cookie.
633
635
  */
634
636
  declare function streamS3ObjectWithRunner(runWithAmplifyServerContext: <T>(args: {
635
- nextServerContext: {
636
- cookies: typeof cookies;
637
- };
637
+ nextServerContext: null;
638
638
  operation: (ctx: AmplifyServerContext) => Promise<T>;
639
639
  }) => Promise<T>, key: string, options: StreamS3Options): Promise<Response>;
640
640
 
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  _resetStreamS3Cache,
8
8
  streamS3Object,
9
9
  streamS3ObjectWithRunner
10
- } from "./chunk-5BLMLMZU.js";
10
+ } from "./chunk-PK7Q7YJX.js";
11
11
 
12
12
  // src/storage.ts
13
13
  import { formatPublicAssetUrl } from "ampless";
@@ -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();
@@ -3,7 +3,6 @@ import 'ampless';
3
3
  import 'next';
4
4
  import 'react';
5
5
  import 'aws-amplify/storage/server';
6
- import 'next/headers';
7
6
 
8
7
  interface Ctx$6 {
9
8
  params: Promise<{
@@ -55,7 +54,7 @@ type RawRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
55
54
  *
56
55
  * Trust model: the body is emitted verbatim, same trust shape as
57
56
  * regular `format: 'html'` posts on the themed route. See
58
- * docs/architecture/04-access-layer-mcp.md §"editor の信頼モデル".
57
+ * https://github.com/heavymoons/ampless/wiki/architecture-04-access-layer-mcp §"editor の信頼モデル".
59
58
  *
60
59
  * Cache-Control: deliberately omitted from the response. Middleware
61
60
  * computes the strategy from `post.metadata.cache` + `post.updatedAt`
@@ -148,4 +147,21 @@ declare function _resetPublicMcpRateLimit(): void;
148
147
  */
149
148
  declare function createPublicMcpRouteHandler(ampless: Ampless): PublicMcpRouteHandlers;
150
149
 
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 };
150
+ interface McpDiscoveryRouteHandlers {
151
+ catalog: {
152
+ GET: (request: Request) => Promise<Response>;
153
+ OPTIONS: (request: Request) => Promise<Response>;
154
+ };
155
+ serverCard: {
156
+ GET: (request: Request) => Promise<Response>;
157
+ OPTIONS: (request: Request) => Promise<Response>;
158
+ };
159
+ }
160
+ /**
161
+ * Build the discovery handlers. The template mounts the catalog handlers
162
+ * at `app/api/mcp/catalog.json/route.ts` (middleware rewrite target) and
163
+ * the server-card handlers at `app/api/mcp/server-card/route.ts`.
164
+ */
165
+ declare function createMcpDiscoveryRouteHandlers(ampless: Ampless): McpDiscoveryRouteHandlers;
166
+
167
+ 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 };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  streamS3Object
3
- } from "../chunk-5BLMLMZU.js";
3
+ } from "../chunk-PK7Q7YJX.js";
4
4
 
5
5
  // src/routes/og.ts
6
6
  import { ImageResponse } from "next/og";
@@ -92,7 +92,6 @@ function createRawRouteHandler(ampless) {
92
92
 
93
93
  // src/routes/static.ts
94
94
  import { createServerRunner } from "@aws-amplify/adapter-nextjs";
95
- import { cookies } from "next/headers";
96
95
  import { getUrl } from "aws-amplify/storage/server";
97
96
  function createStaticRouteHandler(ampless) {
98
97
  const { runWithAmplifyServerContext } = createServerRunner({
@@ -160,7 +159,7 @@ async function serveStaticAsset({
160
159
  presignedUrlFor: (k) => signStaticAsset({ runWithAmplifyServerContext, key: k })
161
160
  };
162
161
  return runWithAmplifyServerContext({
163
- nextServerContext: { cookies },
162
+ nextServerContext: null,
164
163
  operation: (ctx) => streamS3Object(ctx, key, opts)
165
164
  });
166
165
  }
@@ -170,7 +169,7 @@ async function signStaticAsset({
170
169
  }) {
171
170
  try {
172
171
  return await runWithAmplifyServerContext({
173
- nextServerContext: { cookies },
172
+ nextServerContext: null,
174
173
  operation: async (amplifyContext) => {
175
174
  const result = await getUrl(amplifyContext, {
176
175
  path: key,
@@ -344,6 +343,42 @@ import {
344
343
  MAX_BATCH
345
344
  } from "@ampless/mcp-server/jsonrpc";
346
345
  import { publicTools } from "@ampless/mcp-server/public";
346
+
347
+ // src/routes/_mcp-shared.ts
348
+ var PUBLIC_MCP_SERVER_VERSION = "0.2.0";
349
+ function corsHeaders(methods, extra = {}) {
350
+ return {
351
+ "Access-Control-Allow-Origin": "*",
352
+ "Access-Control-Allow-Methods": methods,
353
+ "Access-Control-Allow-Headers": "content-type, mcp-protocol-version",
354
+ "Access-Control-Max-Age": "86400",
355
+ ...extra
356
+ };
357
+ }
358
+ var CARD_NAME_PATTERN = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
359
+ var CARD_NAME_MIN = 3;
360
+ var CARD_NAME_MAX = 200;
361
+ var SERVER_NAME_SUFFIX = "ampless-mcp";
362
+ function resolvePublicMcpIdentity(siteUrl) {
363
+ if (!siteUrl?.trim()) return null;
364
+ let hostname;
365
+ try {
366
+ const parsed = new URL(siteUrl);
367
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
368
+ hostname = parsed.hostname;
369
+ } catch {
370
+ return null;
371
+ }
372
+ const labels = hostname.split(".").filter(Boolean);
373
+ if (labels.length === 0) return null;
374
+ const namespace = labels.reverse().join(".");
375
+ const name = `${namespace}/${SERVER_NAME_SUFFIX}`;
376
+ if (name.length < CARD_NAME_MIN || name.length > CARD_NAME_MAX) return null;
377
+ if (!CARD_NAME_PATTERN.test(name)) return null;
378
+ return { name, version: PUBLIC_MCP_SERVER_VERSION };
379
+ }
380
+
381
+ // src/routes/public-mcp.ts
347
382
  var MAX_BODY_BYTES = 64 * 1024;
348
383
  var RATE_WINDOW_MS = 6e4;
349
384
  var RATE_MAX = 600;
@@ -363,28 +398,23 @@ function retryAfterSeconds() {
363
398
  function _resetPublicMcpRateLimit() {
364
399
  rateState = { count: 0, windowExpires: 0 };
365
400
  }
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
- };
401
+ function corsHeaders2(extra = {}) {
402
+ return corsHeaders("POST, OPTIONS", extra);
374
403
  }
404
+ var STATIC_SERVER_INFO = { name: "ampless-mcp", version: "0.2" };
375
405
  function jsonResponse(status, body) {
376
406
  return new Response(JSON.stringify(body), {
377
407
  status,
378
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
408
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
379
409
  });
380
410
  }
381
411
  function notFound() {
382
- return new Response("Not Found", { status: 404, headers: corsHeaders() });
412
+ return new Response("Not Found", { status: 404, headers: corsHeaders2() });
383
413
  }
384
414
  function rateLimited() {
385
415
  return new Response(JSON.stringify(jsonRpcError(null, -32e3, "Rate limit exceeded")), {
386
416
  status: 429,
387
- headers: corsHeaders({
417
+ headers: corsHeaders2({
388
418
  "Content-Type": "application/json",
389
419
  "Cache-Control": "no-store",
390
420
  "Retry-After": String(retryAfterSeconds())
@@ -396,7 +426,7 @@ function payloadTooLarge() {
396
426
  JSON.stringify(jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Payload too large")),
397
427
  {
398
428
  status: 413,
399
- headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
429
+ headers: corsHeaders2({ "Content-Type": "application/json", "Cache-Control": "no-store" })
400
430
  }
401
431
  );
402
432
  }
@@ -439,6 +469,11 @@ function createPublicMcpRouteHandler(ampless) {
439
469
  function gateOpen() {
440
470
  return ampless.cmsConfig.ai?.publicMcp === true;
441
471
  }
472
+ async function resolveServerInfo() {
473
+ if (ampless.cmsConfig.ai?.mcpDiscovery !== true) return STATIC_SERVER_INFO;
474
+ const settings = await ampless.loadSiteSettings();
475
+ return resolvePublicMcpIdentity(settings.site.url) ?? STATIC_SERVER_INFO;
476
+ }
442
477
  async function POST(request) {
443
478
  try {
444
479
  if (!gateOpen()) return notFound();
@@ -467,7 +502,7 @@ function createPublicMcpRouteHandler(ampless) {
467
502
  const result = await dispatchJsonRpcMessage(parsed, {
468
503
  tools: publicTools,
469
504
  getContext: () => publicCtx,
470
- serverInfo: { name: "ampless-mcp", version: "0.2" },
505
+ serverInfo: await resolveServerInfo(),
471
506
  formatToolError,
472
507
  maxBatch: MAX_BATCH
473
508
  });
@@ -477,7 +512,7 @@ function createPublicMcpRouteHandler(ampless) {
477
512
  if (result.status === "no-content") {
478
513
  return new Response(null, {
479
514
  status: 202,
480
- headers: corsHeaders({ "Cache-Control": "no-store" })
515
+ headers: corsHeaders2({ "Cache-Control": "no-store" })
481
516
  });
482
517
  }
483
518
  return jsonResponse(200, result.body);
@@ -488,15 +523,128 @@ function createPublicMcpRouteHandler(ampless) {
488
523
  }
489
524
  async function OPTIONS(_request) {
490
525
  if (!gateOpen()) return notFound();
491
- return new Response(null, { status: 204, headers: corsHeaders() });
526
+ return new Response(null, { status: 204, headers: corsHeaders2() });
492
527
  }
493
528
  return { POST, OPTIONS };
494
529
  }
530
+
531
+ // src/routes/mcp-discovery.ts
532
+ import { resolvePublicMcpEndpoint as resolvePublicMcpEndpoint2 } from "ampless";
533
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@ampless/mcp-server/jsonrpc";
534
+ var CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
535
+ var CARD_CONTENT_TYPE = "application/mcp-server-card+json";
536
+ var CATALOG_ENTRY_TYPE = "application/mcp-server-card+json";
537
+ var CARD_TEXT_MAX = 100;
538
+ var DESCRIPTION_TEXT = "read-only MCP endpoint for published posts (list, get, search, tags).";
539
+ var CONTROL_CHARS_RE2 = /[\x00-\x1f\x7f]/g;
540
+ function normalizeText2(s) {
541
+ return s.replace(CONTROL_CHARS_RE2, " ").replace(/\s+/g, " ").trim();
542
+ }
543
+ function truncate(s, max) {
544
+ const codePoints = [...s];
545
+ return codePoints.length > max ? codePoints.slice(0, max).join("") : s;
546
+ }
547
+ function discoveryEnabled(ampless) {
548
+ return ampless.cmsConfig.ai?.publicMcp === true && ampless.cmsConfig.ai?.mcpDiscovery === true;
549
+ }
550
+ async function resolveContext(ampless) {
551
+ const settings = await ampless.loadSiteSettings();
552
+ const endpoint = resolvePublicMcpEndpoint2(true, settings.site.url);
553
+ if (typeof endpoint !== "string") return null;
554
+ const identity = resolvePublicMcpIdentity(settings.site.url);
555
+ if (!identity) return null;
556
+ const url = new URL(endpoint);
557
+ const { origin, hostname } = url;
558
+ return {
559
+ origin,
560
+ hostname,
561
+ endpoint: `${origin}/api/mcp`,
562
+ identity,
563
+ siteName: settings.site.name ?? ""
564
+ };
565
+ }
566
+ function notFound2() {
567
+ return new Response("Not Found", {
568
+ status: 404,
569
+ headers: corsHeaders("GET, OPTIONS")
570
+ });
571
+ }
572
+ function jsonResponse2(body, contentType) {
573
+ return new Response(JSON.stringify(body), {
574
+ status: 200,
575
+ headers: corsHeaders("GET, OPTIONS", {
576
+ "Content-Type": contentType,
577
+ // Spec SHOULD value — discovery documents are cheap and stable.
578
+ "Cache-Control": "public, max-age=3600"
579
+ })
580
+ });
581
+ }
582
+ function createMcpDiscoveryRouteHandlers(ampless) {
583
+ async function catalogGET(_request) {
584
+ if (!discoveryEnabled(ampless)) return notFound2();
585
+ const ctx = await resolveContext(ampless);
586
+ if (!ctx) {
587
+ console.warn(
588
+ "[ampless] MCP discovery catalog: site.url is unset or unresolvable \u2014 skipping (404)"
589
+ );
590
+ return notFound2();
591
+ }
592
+ const body = {
593
+ specVersion: "draft",
594
+ entries: [
595
+ {
596
+ // URN tail matches the Card name's `/`-suffix (`ampless-mcp`).
597
+ identifier: `urn:air:${ctx.hostname}:ampless-mcp`,
598
+ type: CATALOG_ENTRY_TYPE,
599
+ url: `${ctx.origin}/api/mcp/server-card`
600
+ }
601
+ ]
602
+ };
603
+ return jsonResponse2(body, "application/json");
604
+ }
605
+ async function serverCardGET(_request) {
606
+ if (!discoveryEnabled(ampless)) return notFound2();
607
+ const ctx = await resolveContext(ampless);
608
+ if (!ctx) {
609
+ console.warn(
610
+ "[ampless] MCP discovery server-card: site.url is unset or unresolvable \u2014 skipping (404)"
611
+ );
612
+ return notFound2();
613
+ }
614
+ const normalizedName = normalizeText2(ctx.siteName);
615
+ const rawDescription = normalizedName ? `${normalizedName} \u2014 ${DESCRIPTION_TEXT}` : DESCRIPTION_TEXT;
616
+ const card = {
617
+ $schema: CARD_SCHEMA_URL,
618
+ name: ctx.identity.name,
619
+ version: ctx.identity.version,
620
+ description: truncate(rawDescription, CARD_TEXT_MAX)
621
+ };
622
+ if (normalizedName) card.title = truncate(normalizedName, CARD_TEXT_MAX);
623
+ card.websiteUrl = ctx.origin;
624
+ card.remotes = [
625
+ {
626
+ type: "streamable-http",
627
+ url: ctx.endpoint,
628
+ supportedProtocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS]
629
+ }
630
+ ];
631
+ return jsonResponse2(card, CARD_CONTENT_TYPE);
632
+ }
633
+ async function optionsHandler(_request) {
634
+ if (!discoveryEnabled(ampless)) return notFound2();
635
+ return new Response(null, { status: 204, headers: corsHeaders("GET, OPTIONS") });
636
+ }
637
+ return {
638
+ catalog: { GET: catalogGET, OPTIONS: optionsHandler },
639
+ serverCard: { GET: serverCardGET, OPTIONS: optionsHandler }
640
+ };
641
+ }
495
642
  export {
496
643
  _resetPublicMcpRateLimit,
497
644
  createFeedRouteHandler,
498
645
  createLlmsTxtRouteHandler,
499
646
  createMarkdownRouteHandler,
647
+ createMcpDiscoveryRouteHandlers,
500
648
  createOgRouteHandler,
501
649
  createPublicMcpRouteHandler,
502
650
  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.78",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,11 +49,11 @@
49
49
  "htmlparser2": "^10.1.0",
50
50
  "lucide-react": "^1.16.0",
51
51
  "marked": "^18.0.4",
52
- "sanitize-html": "^2.17.4",
52
+ "sanitize-html": "^2.18.1",
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.70",
55
+ "@ampless/plugin-og-image": "0.2.0-beta.63",
56
+ "ampless": "1.0.0-beta.63"
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"