@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.
- package/dist/middleware.js +11 -1
- package/dist/routes/index.d.ts +18 -1
- package/dist/routes/index.js +164 -15
- 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
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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"
|