@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.
- package/dist/middleware.js +11 -1
- package/dist/routes/index.d.ts +18 -1
- package/dist/routes/index.js +172 -16
- package/package.json +6 -4
package/dist/middleware.js
CHANGED
|
@@ -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();
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -148,4 +148,21 @@ declare function _resetPublicMcpRateLimit(): void;
|
|
|
148
148
|
*/
|
|
149
149
|
declare function createPublicMcpRouteHandler(ampless: Ampless): PublicMcpRouteHandlers;
|
|
150
150
|
|
|
151
|
-
|
|
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 };
|
package/dist/routes/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
55
|
-
"ampless": "
|
|
56
|
-
"
|
|
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"
|