@ampless/runtime 1.0.0-beta.72 → 1.0.0-beta.73

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
@@ -105,6 +105,19 @@ middleware はリクエストごとに AppSync から `post.format` /
105
105
  `llms.txt` は予約 slug になります — slug が `llms.txt` の投稿はテーマ
106
106
  ページに到達できなくなります。
107
107
 
108
+ `/api/mcp` は **匿名・読み取り専用の MCP エンドポイント**(POST での
109
+ JSON-RPC 2.0)で、`createPublicMcpRouteHandler(ampless)` が生成し
110
+ `app/api/mcp/route.ts` にマウントされます。`@ampless/mcp-server/public`
111
+ の 4 ツール(`list_posts` / `get_post` / `search_posts` / `list_tags`)を
112
+ 公開投稿のみに対して提供します — 書き込み不可・draft 不可視。既定は
113
+ 無効で、`cms.config.ai.publicMcp: true` で有効化します。ルートは open
114
+ CORS(匿名・読み取り専用)、64KB のボディ上限、粗い warm-instance 単位
115
+ の circuit breaker(1 warm lambda あたり 600 req/min。per-IP rate limit
116
+ ではないので、実運用の濫用対策は CloudFront / WAF を併用)を付与します。
117
+ ファクトリは `{ POST, OPTIONS }` を返すので、テンプレートは両方を分割
118
+ 代入します(`export const POST = ...` の一括代入だと CORS preflight が
119
+ 欠落します)。
120
+
108
121
  また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
109
122
  `cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
110
123
  `Cache-Control` を算出してレスポンスに付与します。キャッシュ戦略の
@@ -114,7 +127,7 @@ middleware はリクエストごとに AppSync から `post.format` /
114
127
 
115
128
  - `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
116
129
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
117
- - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`、`createMarkdownRouteHandler`、`createLlmsTxtRouteHandler`
130
+ - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`、`createMarkdownRouteHandler`、`createLlmsTxtRouteHandler`、`createPublicMcpRouteHandler`
118
131
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
119
132
 
120
133
  ## テンプレートに残るもの
package/README.md CHANGED
@@ -104,6 +104,18 @@ a single-file index. Disable with `cms.config.ai.llmsTxt: false`. Note
104
104
  that `llms.txt` becomes a reserved slug either way — a post whose slug
105
105
  happens to be `llms.txt` can no longer reach the themed route.
106
106
 
107
+ `/api/mcp` is an **anonymous, read-only MCP endpoint** (JSON-RPC 2.0
108
+ over POST) built by `createPublicMcpRouteHandler(ampless)` and mounted
109
+ at `app/api/mcp/route.ts`. It serves the four `@ampless/mcp-server/public`
110
+ tools (`list_posts` / `get_post` / `search_posts` / `list_tags`) over
111
+ published posts only — never writes, never sees drafts. Off by default;
112
+ set `cms.config.ai.publicMcp: true` to enable. The route adds open CORS
113
+ (anonymous read-only), a 64KB body cap, and a coarse warm-instance
114
+ circuit breaker (600 req/min per warm lambda, not a per-IP limiter —
115
+ pair it with CloudFront / WAF for real abuse protection). The factory
116
+ returns `{ POST, OPTIONS }`; the template destructures both (a single
117
+ `export const POST = ...` would drop the CORS preflight).
118
+
107
119
  It also computes `Cache-Control` from `post.metadata.cache` (auto /
108
120
  deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
109
121
  freshTtlSeconds, deepTtlSeconds}` and sets the header on the response.
@@ -113,7 +125,7 @@ See `docs/CONTENT.md` for the cache strategy contract.
113
125
 
114
126
  - `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
115
127
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
116
- - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`, `createMarkdownRouteHandler`, `createLlmsTxtRouteHandler`
128
+ - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`, `createMarkdownRouteHandler`, `createLlmsTxtRouteHandler`, `createPublicMcpRouteHandler`
117
129
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
118
130
 
119
131
  ## What's still in the template
@@ -131,4 +131,21 @@ type LlmsTxtRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
131
131
  */
132
132
  declare function createLlmsTxtRouteHandler(ampless: Ampless): LlmsTxtRouteHandler;
133
133
 
134
- export { type FeedRouteHandler, type LlmsTxtRouteHandler, type MarkdownRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createLlmsTxtRouteHandler, createMarkdownRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
134
+ interface PublicMcpRouteHandlers {
135
+ POST: (request: Request) => Promise<Response>;
136
+ OPTIONS: (request: Request) => Promise<Response>;
137
+ }
138
+ /**
139
+ * Test hook: reset the module-scope circuit breaker between cases.
140
+ * Production code never calls this — the counter is intentionally
141
+ * process-local (mirrors middleware's `_resetFlagCache`).
142
+ */
143
+ declare function _resetPublicMcpRateLimit(): void;
144
+ /**
145
+ * Build `{ POST, OPTIONS }` for the public MCP route. The template must
146
+ * destructure both — a single `export const POST = ...(ampless)` assignment
147
+ * would drop OPTIONS and break the CORS preflight.
148
+ */
149
+ declare function createPublicMcpRouteHandler(ampless: Ampless): PublicMcpRouteHandlers;
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 };
@@ -326,11 +326,172 @@ ${lines.join("\n")}`);
326
326
  });
327
327
  };
328
328
  }
329
+
330
+ // src/routes/public-mcp.ts
331
+ import {
332
+ dispatchJsonRpcMessage,
333
+ jsonRpcError,
334
+ JSON_RPC_PARSE_ERROR,
335
+ JSON_RPC_INVALID_REQUEST,
336
+ JSON_RPC_INTERNAL_ERROR,
337
+ MAX_BATCH
338
+ } from "@ampless/mcp-server/jsonrpc";
339
+ import { publicTools } from "@ampless/mcp-server/public";
340
+ var MAX_BODY_BYTES = 64 * 1024;
341
+ var RATE_WINDOW_MS = 6e4;
342
+ var RATE_MAX = 600;
343
+ var rateState = { count: 0, windowExpires: 0 };
344
+ function reserve(n) {
345
+ const now = Date.now();
346
+ if (now >= rateState.windowExpires) {
347
+ rateState = { count: 0, windowExpires: now + RATE_WINDOW_MS };
348
+ }
349
+ if (rateState.count + n > RATE_MAX) return false;
350
+ rateState.count += n;
351
+ return true;
352
+ }
353
+ function retryAfterSeconds() {
354
+ return Math.max(1, Math.ceil((rateState.windowExpires - Date.now()) / 1e3));
355
+ }
356
+ function _resetPublicMcpRateLimit() {
357
+ rateState = { count: 0, windowExpires: 0 };
358
+ }
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
+ };
367
+ }
368
+ function jsonResponse(status, body) {
369
+ return new Response(JSON.stringify(body), {
370
+ status,
371
+ headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
372
+ });
373
+ }
374
+ function notFound() {
375
+ return new Response("Not Found", { status: 404, headers: corsHeaders() });
376
+ }
377
+ function rateLimited() {
378
+ return new Response(JSON.stringify(jsonRpcError(null, -32e3, "Rate limit exceeded")), {
379
+ status: 429,
380
+ headers: corsHeaders({
381
+ "Content-Type": "application/json",
382
+ "Cache-Control": "no-store",
383
+ "Retry-After": String(retryAfterSeconds())
384
+ })
385
+ });
386
+ }
387
+ function payloadTooLarge() {
388
+ return new Response(
389
+ JSON.stringify(jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Payload too large")),
390
+ {
391
+ status: 413,
392
+ headers: corsHeaders({ "Content-Type": "application/json", "Cache-Control": "no-store" })
393
+ }
394
+ );
395
+ }
396
+ async function readBodyCapped(request) {
397
+ const body = request.body;
398
+ if (!body) return "";
399
+ const reader = body.getReader();
400
+ const chunks = [];
401
+ let total = 0;
402
+ for (; ; ) {
403
+ const { done, value } = await reader.read();
404
+ if (done) break;
405
+ if (!value) continue;
406
+ total += value.byteLength;
407
+ if (total > MAX_BODY_BYTES) {
408
+ await reader.cancel();
409
+ return null;
410
+ }
411
+ chunks.push(value);
412
+ }
413
+ if (total === 0) return "";
414
+ const merged = new Uint8Array(total);
415
+ let offset = 0;
416
+ for (const chunk of chunks) {
417
+ merged.set(chunk, offset);
418
+ offset += chunk.byteLength;
419
+ }
420
+ return new TextDecoder("utf-8").decode(merged);
421
+ }
422
+ function createPublicMcpRouteHandler(ampless) {
423
+ const publicCtx = {
424
+ listPublishedPosts: (opts) => ampless.listPublishedPosts(opts),
425
+ getPublishedPost: (slug) => ampless.getPublishedPost(slug),
426
+ postToMarkdown: (post, opts) => ampless.postToMarkdown(post, opts)
427
+ };
428
+ function formatToolError(err) {
429
+ console.error("[ampless] public MCP tool error", err);
430
+ return "Internal error while executing the tool.";
431
+ }
432
+ function gateOpen() {
433
+ return ampless.cmsConfig.ai?.publicMcp === true;
434
+ }
435
+ async function POST(request) {
436
+ try {
437
+ if (!gateOpen()) return notFound();
438
+ if (!reserve(1)) return rateLimited();
439
+ const contentLength = request.headers.get("content-length");
440
+ if (contentLength) {
441
+ const declared = Number(contentLength);
442
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
443
+ return payloadTooLarge();
444
+ }
445
+ }
446
+ const bodyText = await readBodyCapped(request);
447
+ if (bodyText === null) return payloadTooLarge();
448
+ if (bodyText.length === 0) {
449
+ return jsonResponse(400, jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Empty body"));
450
+ }
451
+ let parsed;
452
+ try {
453
+ parsed = JSON.parse(bodyText);
454
+ } catch {
455
+ return jsonResponse(400, jsonRpcError(null, JSON_RPC_PARSE_ERROR, "Parse error"));
456
+ }
457
+ if (Array.isArray(parsed) && parsed.length >= 1 && parsed.length <= MAX_BATCH) {
458
+ if (!reserve(parsed.length - 1)) return rateLimited();
459
+ }
460
+ const result = await dispatchJsonRpcMessage(parsed, {
461
+ tools: publicTools,
462
+ getContext: () => publicCtx,
463
+ serverInfo: { name: "ampless-mcp", version: "0.2" },
464
+ formatToolError,
465
+ maxBatch: MAX_BATCH
466
+ });
467
+ if (result.status === "invalid") {
468
+ return jsonResponse(400, result.body);
469
+ }
470
+ if (result.status === "no-content") {
471
+ return new Response(null, {
472
+ status: 202,
473
+ headers: corsHeaders({ "Cache-Control": "no-store" })
474
+ });
475
+ }
476
+ return jsonResponse(200, result.body);
477
+ } catch (err) {
478
+ console.error("[ampless] public MCP route error", err);
479
+ return jsonResponse(500, jsonRpcError(null, JSON_RPC_INTERNAL_ERROR, "Internal error"));
480
+ }
481
+ }
482
+ async function OPTIONS(_request) {
483
+ if (!gateOpen()) return notFound();
484
+ return new Response(null, { status: 204, headers: corsHeaders() });
485
+ }
486
+ return { POST, OPTIONS };
487
+ }
329
488
  export {
489
+ _resetPublicMcpRateLimit,
330
490
  createFeedRouteHandler,
331
491
  createLlmsTxtRouteHandler,
332
492
  createMarkdownRouteHandler,
333
493
  createOgRouteHandler,
494
+ createPublicMcpRouteHandler,
334
495
  createRawRouteHandler,
335
496
  createSitemapRouteHandler,
336
497
  createStaticRouteHandler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-beta.72",
3
+ "version": "1.0.0-beta.73",
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,8 +51,9 @@
51
51
  "marked": "^18.0.4",
52
52
  "sanitize-html": "^2.17.4",
53
53
  "tailwind-merge": "^3.6.0",
54
- "@ampless/plugin-og-image": "0.2.0-beta.59",
55
- "ampless": "1.0.0-beta.59"
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"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "@aws-amplify/adapter-nextjs": "^1",