@vantageos/mcp-boilerplate 0.7.0 → 0.9.0
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/README.fr.md +57 -5
- package/README.md +55 -5
- package/dist/build-info.json +1 -0
- package/dist/buildIdentity.d.ts +16 -0
- package/dist/buildIdentity.js +39 -0
- package/dist/embedded.generated.d.ts +225 -0
- package/dist/embedded.generated.js +8 -0
- package/dist/http.js +5 -3
- package/dist/httpApp.d.ts +6 -0
- package/dist/httpApp.js +64 -7
- package/dist/i18n.js +6 -16
- package/dist/lake/convex.d.ts +4 -0
- package/dist/lake/convex.js +10 -3
- package/dist/lake/reader.d.ts +35 -2
- package/dist/lake/reader.js +113 -2
- package/dist/landingPage.d.ts +24 -0
- package/dist/landingPage.js +109 -0
- package/dist/mcpServerFactory.d.ts +11 -0
- package/dist/mcpServerFactory.js +28 -7
- package/dist/servedToolRecords.d.ts +6 -0
- package/dist/servedToolRecords.js +11 -1
- package/dist/serverCard.d.ts +68 -0
- package/dist/serverCard.js +68 -0
- package/dist/serverIcon.d.ts +17 -0
- package/dist/serverIcon.js +26 -0
- package/dist/serverIdentity.d.ts +83 -0
- package/dist/serverIdentity.js +130 -0
- package/dist/ui/uiResource.d.ts +14 -1
- package/dist/ui/uiResource.js +11 -1
- package/dist/version.js +6 -5
- package/docs/manufact-gap.md +200 -0
- package/mcp-server.json +1 -1
- package/package.json +3 -2
- package/plugin.json +2 -4
- package/scripts/readme-fr.mjs +8 -3
- package/dist/locales/en.json +0 -88
- package/dist/locales/fr.json +0 -88
package/dist/mcpServerFactory.js
CHANGED
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
|
|
32
32
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
33
33
|
import { z } from "zod";
|
|
34
|
-
import { getBuildIdentity } from "./buildIdentity.js";
|
|
34
|
+
import { getBuildCommit, getBuildIdentity } from "./buildIdentity.js";
|
|
35
35
|
import { t } from "./i18n.js";
|
|
36
36
|
import { EXAMPLE_ITEMS_READER, LAKE_UNREADABLE_CODE, LakeReadError, } from "./lake/reader.js";
|
|
37
37
|
import { rejectCursorOnSinglePageLists } from "./listCursor.js";
|
|
38
38
|
import { INVALID_CURSOR_CODE, InvalidCursorError, PAGINATION, readPage } from "./pagination.js";
|
|
39
39
|
import { RATE_LIMITED_CODE, toolRateLimiter } from "./rateLimit.js";
|
|
40
40
|
import { trackToolRecords } from "./servedToolRecords.js";
|
|
41
|
+
import { getServerIdentity, serverInstructions, serverPresentation } from "./serverIdentity.js";
|
|
41
42
|
import { loadServerManifest } from "./serverManifest.js";
|
|
42
43
|
import { scopeRowsForTenant } from "./tenancy.js";
|
|
43
44
|
import { LIST_ITEMS_ANNOTATIONS, PING_ANNOTATIONS, WHOAMI_ANNOTATIONS } from "./toolRecord.js";
|
|
@@ -120,18 +121,26 @@ export function createBoilerplateMcpServer(deps = {}) {
|
|
|
120
121
|
// at registration and checked against it (and the closed mechanism set) before the server is
|
|
121
122
|
// returned: the check covers what is served, there is no list to keep in step.
|
|
122
123
|
const manifest = loadServerManifest();
|
|
123
|
-
// The identity the server announces: version
|
|
124
|
-
//
|
|
125
|
-
// (
|
|
126
|
-
//
|
|
124
|
+
// The identity the server announces (S-49): name, version, title, description (in the caller's
|
|
125
|
+
// locale), websiteUrl and icons, all from src/serverIdentity.ts (package.json or environment),
|
|
126
|
+
// plus the build identity (C-25): buildId = the git commit the tree was built from, buildHash =
|
|
127
|
+
// the hash of the files this process runs from (src/buildIdentity.ts). The SDK sends
|
|
128
|
+
// `serverInfo` as given, on `server/discover` (2026-07-28,
|
|
129
|
+
// `_meta["io.modelcontextprotocol/serverInfo"]`) and on `initialize` (2025-era). The build keys
|
|
130
|
+
// are not modelled `Implementation` keys, hence the spread (no excess-key check).
|
|
127
131
|
const server = new McpServer({
|
|
128
132
|
name: SERVER_NAME,
|
|
129
133
|
version: SERVER_VERSION,
|
|
130
|
-
...
|
|
134
|
+
...serverPresentation(getServerIdentity(), locale, deps.baseUrl),
|
|
135
|
+
...{
|
|
136
|
+
buildId: getBuildCommit(),
|
|
137
|
+
buildHash: getBuildIdentity(),
|
|
138
|
+
},
|
|
131
139
|
}, {
|
|
132
140
|
// The capabilities come from the server manifest (mcp-server.json, C-18): one source.
|
|
133
141
|
capabilities: manifest.capabilities,
|
|
134
|
-
|
|
142
|
+
// How an agent uses this server, in the caller's locale (src/serverIdentity.ts).
|
|
143
|
+
instructions: serverInstructions(locale),
|
|
135
144
|
supportedProtocolVersions: SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
|
|
136
145
|
cacheHints: {
|
|
137
146
|
"tools/list": { ttlMs: 60_000, cacheScope: "public" },
|
|
@@ -302,5 +311,17 @@ export function createBoilerplateMcpServer(deps = {}) {
|
|
|
302
311
|
rejectCursorOnSinglePageLists(server);
|
|
303
312
|
// Start refusal: every registered tool's record, as registered, must be sound.
|
|
304
313
|
served.assertServed(manifest.contextCeiling);
|
|
314
|
+
SUMMARIES.set(server, served.summaries);
|
|
305
315
|
return server;
|
|
306
316
|
}
|
|
317
|
+
const SUMMARIES = new WeakMap();
|
|
318
|
+
/**
|
|
319
|
+
* The tools a server built by this factory registered (name, title, description), for the public
|
|
320
|
+
* server card (src/serverCard.ts): read from the registrations, never a second list.
|
|
321
|
+
*/
|
|
322
|
+
export function registeredToolSummaries(server) {
|
|
323
|
+
const summaries = SUMMARIES.get(server);
|
|
324
|
+
if (!summaries)
|
|
325
|
+
throw new Error("registeredToolSummaries: not a server built by this factory");
|
|
326
|
+
return summaries;
|
|
327
|
+
}
|
|
@@ -12,6 +12,12 @@ import type { ContextCeiling } from "./serverManifest.js";
|
|
|
12
12
|
export interface ServedToolRecords {
|
|
13
13
|
/** The annotations of every tool registered so far, by tool name. */
|
|
14
14
|
readonly records: Readonly<Record<string, Record<string, unknown>>>;
|
|
15
|
+
/** Each registered tool's name, title and description, in registration order (the server card). */
|
|
16
|
+
readonly summaries: ReadonlyArray<{
|
|
17
|
+
name: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
15
21
|
/** Throws `ToolRecordError` naming every unsound tool; same check as the wire test. */
|
|
16
22
|
assertServed(ceiling: ContextCeiling): void;
|
|
17
23
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { assertToolRecords } from "./toolRecord.js";
|
|
2
2
|
export function trackToolRecords(server) {
|
|
3
3
|
const records = {};
|
|
4
|
+
const summaries = [];
|
|
4
5
|
const register = server.registerTool.bind(server);
|
|
5
6
|
server.registerTool = ((name, config, callback) => {
|
|
6
7
|
records[name] = { ...(config.annotations ?? {}) };
|
|
8
|
+
summaries.push({
|
|
9
|
+
name,
|
|
10
|
+
...(config.title !== undefined ? { title: config.title } : {}),
|
|
11
|
+
...(config.description !== undefined ? { description: config.description } : {}),
|
|
12
|
+
});
|
|
7
13
|
return register(name, config, callback);
|
|
8
14
|
});
|
|
9
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
records,
|
|
17
|
+
summaries,
|
|
18
|
+
assertServed: (ceiling) => assertToolRecords(records, ceiling),
|
|
19
|
+
};
|
|
10
20
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public MCP Server Card (mcp-server-standard S-50, Manufact "mcp.json or MCP Server Card
|
|
3
|
+
* served"). Served by src/httpApp.ts, before every gate, at each path in `SERVER_CARD_PATHS`.
|
|
4
|
+
*
|
|
5
|
+
* Shape: the `ServerCard` of the MCP Server Card extension (SEP-2127,
|
|
6
|
+
* modelcontextprotocol/experimental-ext-server-card `schema.json` at commit
|
|
7
|
+
* 526201bbc80231daa40ffcdecfc9da4e54e5dc93, sha256
|
|
8
|
+
* 2c772b51edb367f154771d84ddbae87ddba00a624422c8e46f218a9ac03bf042), vendored at
|
|
9
|
+
* tests/fixtures/server-card.schema.json and validated in tests/serverMetadata.test.ts.
|
|
10
|
+
*
|
|
11
|
+
* Declared divergence: the extension says a card does not enumerate primitives, while Manufact's
|
|
12
|
+
* check and mcp-doctor S-50 read `capabilities` and a tool summary (`tools`) on the card. The
|
|
13
|
+
* schema allows additional members, so the card carries both: the capabilities of
|
|
14
|
+
* mcp-server.json (the same object `initialize` serves) and each served tool's name, title and
|
|
15
|
+
* description, read from the tools as registered — never a second list to keep in step.
|
|
16
|
+
*
|
|
17
|
+
* The card is public metadata and grants nothing. Every string comes from src/serverIdentity.ts
|
|
18
|
+
* (the source of the serverInfo), and the URLs from the origin the server is reached at.
|
|
19
|
+
*/
|
|
20
|
+
import type { Icon } from "@modelcontextprotocol/server";
|
|
21
|
+
import { type ServerIdentity } from "./serverIdentity.js";
|
|
22
|
+
export declare const SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
|
|
23
|
+
export declare const SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json";
|
|
24
|
+
/**
|
|
25
|
+
* Where the card is served: `/mcp/server-card` is the location the extension reserves
|
|
26
|
+
* (`<streamable-http-url>/server-card`); the others are the origin-level paths Manufact and
|
|
27
|
+
* mcp-doctor S-50 probe. Manufact also reads `/.well-known/ai-catalog.json` and the parent
|
|
28
|
+
* domain; an AI Catalog lists cards rather than being one, so it is not served here.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SERVER_CARD_PATHS: readonly ["/mcp/server-card", "/.well-known/mcp/server-card.json", "/.well-known/mcp-server-card", "/mcp.json", "/.well-known/mcp.json"];
|
|
31
|
+
/** One served tool, as summarised on the card. */
|
|
32
|
+
export interface CardTool {
|
|
33
|
+
name: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ServerCard {
|
|
38
|
+
$schema: string;
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
description: string;
|
|
42
|
+
title: string;
|
|
43
|
+
websiteUrl: string;
|
|
44
|
+
icons: Icon[];
|
|
45
|
+
remotes: Array<{
|
|
46
|
+
type: "streamable-http";
|
|
47
|
+
url: string;
|
|
48
|
+
}>;
|
|
49
|
+
capabilities: Record<string, unknown>;
|
|
50
|
+
tools: CardTool[];
|
|
51
|
+
}
|
|
52
|
+
export interface CardInput {
|
|
53
|
+
identity: ServerIdentity;
|
|
54
|
+
/** The served MCP endpoint URL. */
|
|
55
|
+
mcpUrl: string;
|
|
56
|
+
/** The base URL the icon is served under (ends in `/`). */
|
|
57
|
+
baseUrl: string;
|
|
58
|
+
capabilities: Record<string, unknown>;
|
|
59
|
+
tools: CardTool[];
|
|
60
|
+
}
|
|
61
|
+
export declare function buildServerCard(input: CardInput): ServerCard;
|
|
62
|
+
/**
|
|
63
|
+
* The card as an HTTP response: the card media type when the client asks for it, JSON otherwise.
|
|
64
|
+
* `cacheable` is false when the origin was read from the
|
|
65
|
+
* request (no configured public base URL): a shared cache must not keep a card naming a Host
|
|
66
|
+
* header another client sent.
|
|
67
|
+
*/
|
|
68
|
+
export declare function serverCardResponse(req: Request, card: ServerCard, cacheable: boolean): Response;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public MCP Server Card (mcp-server-standard S-50, Manufact "mcp.json or MCP Server Card
|
|
3
|
+
* served"). Served by src/httpApp.ts, before every gate, at each path in `SERVER_CARD_PATHS`.
|
|
4
|
+
*
|
|
5
|
+
* Shape: the `ServerCard` of the MCP Server Card extension (SEP-2127,
|
|
6
|
+
* modelcontextprotocol/experimental-ext-server-card `schema.json` at commit
|
|
7
|
+
* 526201bbc80231daa40ffcdecfc9da4e54e5dc93, sha256
|
|
8
|
+
* 2c772b51edb367f154771d84ddbae87ddba00a624422c8e46f218a9ac03bf042), vendored at
|
|
9
|
+
* tests/fixtures/server-card.schema.json and validated in tests/serverMetadata.test.ts.
|
|
10
|
+
*
|
|
11
|
+
* Declared divergence: the extension says a card does not enumerate primitives, while Manufact's
|
|
12
|
+
* check and mcp-doctor S-50 read `capabilities` and a tool summary (`tools`) on the card. The
|
|
13
|
+
* schema allows additional members, so the card carries both: the capabilities of
|
|
14
|
+
* mcp-server.json (the same object `initialize` serves) and each served tool's name, title and
|
|
15
|
+
* description, read from the tools as registered — never a second list to keep in step.
|
|
16
|
+
*
|
|
17
|
+
* The card is public metadata and grants nothing. Every string comes from src/serverIdentity.ts
|
|
18
|
+
* (the source of the serverInfo), and the URLs from the origin the server is reached at.
|
|
19
|
+
*/
|
|
20
|
+
import { cardDescription, cardName, serverIcons } from "./serverIdentity.js";
|
|
21
|
+
export const SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
|
|
22
|
+
export const SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json";
|
|
23
|
+
/**
|
|
24
|
+
* Where the card is served: `/mcp/server-card` is the location the extension reserves
|
|
25
|
+
* (`<streamable-http-url>/server-card`); the others are the origin-level paths Manufact and
|
|
26
|
+
* mcp-doctor S-50 probe. Manufact also reads `/.well-known/ai-catalog.json` and the parent
|
|
27
|
+
* domain; an AI Catalog lists cards rather than being one, so it is not served here.
|
|
28
|
+
*/
|
|
29
|
+
export const SERVER_CARD_PATHS = [
|
|
30
|
+
"/mcp/server-card",
|
|
31
|
+
"/.well-known/mcp/server-card.json",
|
|
32
|
+
"/.well-known/mcp-server-card",
|
|
33
|
+
"/mcp.json",
|
|
34
|
+
"/.well-known/mcp.json",
|
|
35
|
+
];
|
|
36
|
+
export function buildServerCard(input) {
|
|
37
|
+
const { identity } = input;
|
|
38
|
+
return {
|
|
39
|
+
$schema: SERVER_CARD_SCHEMA_URL,
|
|
40
|
+
name: cardName(identity.websiteUrl, identity.name),
|
|
41
|
+
version: identity.version,
|
|
42
|
+
description: cardDescription(identity.description.en),
|
|
43
|
+
title: identity.title,
|
|
44
|
+
websiteUrl: identity.websiteUrl,
|
|
45
|
+
icons: serverIcons(input.baseUrl),
|
|
46
|
+
remotes: [{ type: "streamable-http", url: input.mcpUrl }],
|
|
47
|
+
capabilities: input.capabilities,
|
|
48
|
+
tools: input.tools,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The card as an HTTP response: the card media type when the client asks for it, JSON otherwise.
|
|
53
|
+
* `cacheable` is false when the origin was read from the
|
|
54
|
+
* request (no configured public base URL): a shared cache must not keep a card naming a Host
|
|
55
|
+
* header another client sent.
|
|
56
|
+
*/
|
|
57
|
+
export function serverCardResponse(req, card, cacheable) {
|
|
58
|
+
const accept = req.headers.get("accept") ?? "";
|
|
59
|
+
const headers = new Headers({
|
|
60
|
+
"content-type": accept.includes(SERVER_CARD_MEDIA_TYPE)
|
|
61
|
+
? SERVER_CARD_MEDIA_TYPE
|
|
62
|
+
: "application/json",
|
|
63
|
+
"cache-control": cacheable ? "public, max-age=3600" : "no-store",
|
|
64
|
+
"access-control-allow-origin": "*",
|
|
65
|
+
"x-content-type-options": "nosniff",
|
|
66
|
+
});
|
|
67
|
+
return new Response(JSON.stringify(card), { status: 200, headers });
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server icon (MCP `Implementation.icons`, Manufact "Scalable or high-resolution icon"). A
|
|
3
|
+
* vector, so it is sharp at every size a host renders and needs no `sizes` beyond `"any"`.
|
|
4
|
+
*
|
|
5
|
+
* A fork replaces `SERVER_ICON_SVG` with its own mark. Keep it a self-contained SVG: no script,
|
|
6
|
+
* no external reference (the route serves it under a CSP that forbids both), because a host may
|
|
7
|
+
* render it in its own page.
|
|
8
|
+
*
|
|
9
|
+
* Served by src/httpApp.ts at `SERVER_ICON_PATH`, before any authentication, and inlined as a
|
|
10
|
+
* `data:` URI where there is no HTTP origin (stdio).
|
|
11
|
+
*/
|
|
12
|
+
/** Where the HTTP transport serves the icon, relative to the public base URL. */
|
|
13
|
+
export declare const SERVER_ICON_PATH = "icon.svg";
|
|
14
|
+
export declare const SERVER_ICON_MIME_TYPE = "image/svg+xml";
|
|
15
|
+
export declare const SERVER_ICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\" role=\"img\" aria-label=\"MCP server\">\n<rect width=\"128\" height=\"128\" rx=\"28\" fill=\"#1f2a44\"/>\n<path d=\"M40 44 L88 44 M40 44 L64 88 M88 44 L64 88\" stroke=\"#8fb3ff\" stroke-width=\"8\" stroke-linecap=\"round\" fill=\"none\"/>\n<circle cx=\"40\" cy=\"44\" r=\"12\" fill=\"#ffffff\"/>\n<circle cx=\"88\" cy=\"44\" r=\"12\" fill=\"#ffffff\"/>\n<circle cx=\"64\" cy=\"88\" r=\"12\" fill=\"#ffffff\"/>\n</svg>\n";
|
|
16
|
+
/** The icon as a `data:` URI, for a transport with no HTTP origin to serve it from. */
|
|
17
|
+
export declare function serverIconDataUri(): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server icon (MCP `Implementation.icons`, Manufact "Scalable or high-resolution icon"). A
|
|
3
|
+
* vector, so it is sharp at every size a host renders and needs no `sizes` beyond `"any"`.
|
|
4
|
+
*
|
|
5
|
+
* A fork replaces `SERVER_ICON_SVG` with its own mark. Keep it a self-contained SVG: no script,
|
|
6
|
+
* no external reference (the route serves it under a CSP that forbids both), because a host may
|
|
7
|
+
* render it in its own page.
|
|
8
|
+
*
|
|
9
|
+
* Served by src/httpApp.ts at `SERVER_ICON_PATH`, before any authentication, and inlined as a
|
|
10
|
+
* `data:` URI where there is no HTTP origin (stdio).
|
|
11
|
+
*/
|
|
12
|
+
/** Where the HTTP transport serves the icon, relative to the public base URL. */
|
|
13
|
+
export const SERVER_ICON_PATH = "icon.svg";
|
|
14
|
+
export const SERVER_ICON_MIME_TYPE = "image/svg+xml";
|
|
15
|
+
export const SERVER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="MCP server">
|
|
16
|
+
<rect width="128" height="128" rx="28" fill="#1f2a44"/>
|
|
17
|
+
<path d="M40 44 L88 44 M40 44 L64 88 M88 44 L64 88" stroke="#8fb3ff" stroke-width="8" stroke-linecap="round" fill="none"/>
|
|
18
|
+
<circle cx="40" cy="44" r="12" fill="#ffffff"/>
|
|
19
|
+
<circle cx="88" cy="44" r="12" fill="#ffffff"/>
|
|
20
|
+
<circle cx="64" cy="88" r="12" fill="#ffffff"/>
|
|
21
|
+
</svg>
|
|
22
|
+
`;
|
|
23
|
+
/** The icon as a `data:` URI, for a transport with no HTTP origin to serve it from. */
|
|
24
|
+
export function serverIconDataUri() {
|
|
25
|
+
return `data:${SERVER_ICON_MIME_TYPE};base64,${Buffer.from(SERVER_ICON_SVG, "utf8").toString("base64")}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server's public identity: the ONE source of every string a host reads about this server
|
|
3
|
+
* before or at connection — the `serverInfo` of `initialize` / `server/discover`
|
|
4
|
+
* (src/mcpServerFactory.ts), its `instructions`, the MCP Server Card (src/serverCard.ts) and the
|
|
5
|
+
* setup page at the MCP URL (src/landingPage.ts). No copy can drift from another.
|
|
6
|
+
*
|
|
7
|
+
* Every value is DERIVED, never a literal a fork must remember to edit:
|
|
8
|
+
*
|
|
9
|
+
* | Item | Environment override | Otherwise, package.json |
|
|
10
|
+
* |----------------|-------------------------------|-------------------------|
|
|
11
|
+
* | title | `MCP_SERVER_TITLE` | `displayName` |
|
|
12
|
+
* | description EN | `MCP_SERVER_DESCRIPTION` | `description` |
|
|
13
|
+
* | description FR | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
|
|
14
|
+
* | website | `MCP_SERVER_WEBSITE_URL` | `homepage` |
|
|
15
|
+
*
|
|
16
|
+
* `name` and `version` keep their own sources (src/version.ts). The template's package.json
|
|
17
|
+
* carries the template's values; a fork edits package.json (or sets the variables) and every
|
|
18
|
+
* surface follows. A value neither source carries is refused at start, naming the variable AND
|
|
19
|
+
* the package.json field, in English and French: a served placeholder is what Manufact and
|
|
20
|
+
* mcp-doctor S-49 score as missing, so the server never invents one.
|
|
21
|
+
*/
|
|
22
|
+
import type { Icon, Implementation } from "@modelcontextprotocol/server";
|
|
23
|
+
import { type McpLocale } from "./i18n.js";
|
|
24
|
+
/** The package.json fields the identity reads. */
|
|
25
|
+
export interface PackageIdentityFields {
|
|
26
|
+
displayName?: unknown;
|
|
27
|
+
description?: unknown;
|
|
28
|
+
description_fr?: unknown;
|
|
29
|
+
homepage?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface ServerIdentity {
|
|
32
|
+
name: string;
|
|
33
|
+
version: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description: Record<McpLocale, string>;
|
|
36
|
+
websiteUrl: string;
|
|
37
|
+
}
|
|
38
|
+
/** The environment variables, by identity item (documented in README and .env.example). */
|
|
39
|
+
export declare const IDENTITY_ENV: {
|
|
40
|
+
readonly title: "MCP_SERVER_TITLE";
|
|
41
|
+
readonly description: "MCP_SERVER_DESCRIPTION";
|
|
42
|
+
readonly descriptionFr: "MCP_SERVER_DESCRIPTION_FR";
|
|
43
|
+
readonly websiteUrl: "MCP_SERVER_WEBSITE_URL";
|
|
44
|
+
};
|
|
45
|
+
/** Resolves the identity from `env` and the package.json fields. Throws, naming what is missing. */
|
|
46
|
+
export declare function resolveServerIdentity(env: NodeJS.ProcessEnv, pkg: PackageIdentityFields): ServerIdentity;
|
|
47
|
+
/**
|
|
48
|
+
* The identity of THIS process: the package fields embedded at build (src/embedded.generated.ts, from
|
|
49
|
+
* package.json; no file read, so a bundled server has them too) and process.env. Once.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getServerIdentity(): ServerIdentity;
|
|
52
|
+
/**
|
|
53
|
+
* The icons a host reads. With an HTTP base URL (the origin the server is reached at, ending in
|
|
54
|
+
* `/`), the icon is the URL src/httpApp.ts serves; without one (stdio), it travels inline.
|
|
55
|
+
*/
|
|
56
|
+
export declare function serverIcons(baseUrl?: string): Icon[];
|
|
57
|
+
/**
|
|
58
|
+
* The `serverInfo` (MCP `Implementation`) items beyond name and version, for a caller's locale.
|
|
59
|
+
* The factory writes `name` and `version` beside them from src/version.ts, where the
|
|
60
|
+
* version-observability check (D16) reads them.
|
|
61
|
+
*/
|
|
62
|
+
export declare function serverPresentation(identity: ServerIdentity, locale: McpLocale, baseUrl?: string): Omit<Implementation, "name" | "version">;
|
|
63
|
+
/**
|
|
64
|
+
* The server instructions (`InitializeResult.instructions` / `DiscoverResult.instructions`): how
|
|
65
|
+
* an agent should use THIS server, critical guidance first, from the locale files
|
|
66
|
+
* (`Mcp.server.instructions`). A fork rewrites them there for its own tools;
|
|
67
|
+
* tests/serverMetadata.test.ts fails when a served tool goes unnamed or the text passes Claude
|
|
68
|
+
* Code's 2048-byte field budget.
|
|
69
|
+
*/
|
|
70
|
+
export declare function serverInstructions(locale: McpLocale): string;
|
|
71
|
+
/**
|
|
72
|
+
* The card's reverse-DNS name, derived from the website: `io.github.<owner>/<name>` for a GitHub
|
|
73
|
+
* website (the MCP Registry convention), the reversed host otherwise (`www.` dropped).
|
|
74
|
+
*/
|
|
75
|
+
export declare function cardName(websiteUrl: string, serverName: string): string;
|
|
76
|
+
/** The card schema's `description` cap. */
|
|
77
|
+
export declare const CARD_DESCRIPTION_MAX = 100;
|
|
78
|
+
/**
|
|
79
|
+
* The card description: the English description whole when it fits the schema's 100 characters;
|
|
80
|
+
* otherwise cut on the last word boundary that leaves room for an ellipsis. Declared divergence:
|
|
81
|
+
* the card then carries a shortened text, the serverInfo the full one.
|
|
82
|
+
*/
|
|
83
|
+
export declare function cardDescription(description: string): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server's public identity: the ONE source of every string a host reads about this server
|
|
3
|
+
* before or at connection — the `serverInfo` of `initialize` / `server/discover`
|
|
4
|
+
* (src/mcpServerFactory.ts), its `instructions`, the MCP Server Card (src/serverCard.ts) and the
|
|
5
|
+
* setup page at the MCP URL (src/landingPage.ts). No copy can drift from another.
|
|
6
|
+
*
|
|
7
|
+
* Every value is DERIVED, never a literal a fork must remember to edit:
|
|
8
|
+
*
|
|
9
|
+
* | Item | Environment override | Otherwise, package.json |
|
|
10
|
+
* |----------------|-------------------------------|-------------------------|
|
|
11
|
+
* | title | `MCP_SERVER_TITLE` | `displayName` |
|
|
12
|
+
* | description EN | `MCP_SERVER_DESCRIPTION` | `description` |
|
|
13
|
+
* | description FR | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
|
|
14
|
+
* | website | `MCP_SERVER_WEBSITE_URL` | `homepage` |
|
|
15
|
+
*
|
|
16
|
+
* `name` and `version` keep their own sources (src/version.ts). The template's package.json
|
|
17
|
+
* carries the template's values; a fork edits package.json (or sets the variables) and every
|
|
18
|
+
* surface follows. A value neither source carries is refused at start, naming the variable AND
|
|
19
|
+
* the package.json field, in English and French: a served placeholder is what Manufact and
|
|
20
|
+
* mcp-doctor S-49 score as missing, so the server never invents one.
|
|
21
|
+
*/
|
|
22
|
+
import { isSecureUrl } from "./declaredVariables.js";
|
|
23
|
+
import { EMBEDDED_PACKAGE_IDENTITY } from "./embedded.generated.js";
|
|
24
|
+
import { t, tBoth } from "./i18n.js";
|
|
25
|
+
import { SERVER_ICON_MIME_TYPE, SERVER_ICON_PATH, serverIconDataUri } from "./serverIcon.js";
|
|
26
|
+
import { SERVER_NAME, SERVER_VERSION } from "./version.js";
|
|
27
|
+
/** The environment variables, by identity item (documented in README and .env.example). */
|
|
28
|
+
export const IDENTITY_ENV = {
|
|
29
|
+
title: "MCP_SERVER_TITLE",
|
|
30
|
+
description: "MCP_SERVER_DESCRIPTION",
|
|
31
|
+
descriptionFr: "MCP_SERVER_DESCRIPTION_FR",
|
|
32
|
+
websiteUrl: "MCP_SERVER_WEBSITE_URL",
|
|
33
|
+
};
|
|
34
|
+
function pick(env, pkg, item, variable, field) {
|
|
35
|
+
const fromEnv = env[variable];
|
|
36
|
+
if (fromEnv !== undefined && fromEnv.trim() !== "")
|
|
37
|
+
return { value: fromEnv, source: variable };
|
|
38
|
+
const fromPkg = pkg[field];
|
|
39
|
+
if (typeof fromPkg === "string" && fromPkg.trim() !== "")
|
|
40
|
+
return { value: fromPkg, source: `package.json ${field}` };
|
|
41
|
+
throw new Error(tBoth("server.identityMissing", { item, variable, field }));
|
|
42
|
+
}
|
|
43
|
+
/** Resolves the identity from `env` and the package.json fields. Throws, naming what is missing. */
|
|
44
|
+
export function resolveServerIdentity(env, pkg) {
|
|
45
|
+
const title = pick(env, pkg, "title", IDENTITY_ENV.title, "displayName");
|
|
46
|
+
const en = pick(env, pkg, "description", IDENTITY_ENV.description, "description");
|
|
47
|
+
const fr = pick(env, pkg, "description_fr", IDENTITY_ENV.descriptionFr, "description_fr");
|
|
48
|
+
const site = pick(env, pkg, "websiteUrl", IDENTITY_ENV.websiteUrl, "homepage");
|
|
49
|
+
if (!isSecureUrl(site.value))
|
|
50
|
+
throw new Error(tBoth("server.identityInvalid", { item: "websiteUrl", source: site.source }));
|
|
51
|
+
return {
|
|
52
|
+
name: SERVER_NAME,
|
|
53
|
+
version: SERVER_VERSION,
|
|
54
|
+
title: title.value,
|
|
55
|
+
description: { en: en.value, fr: fr.value },
|
|
56
|
+
websiteUrl: site.value,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let processIdentity;
|
|
60
|
+
/**
|
|
61
|
+
* The identity of THIS process: the package fields embedded at build (src/embedded.generated.ts, from
|
|
62
|
+
* package.json; no file read, so a bundled server has them too) and process.env. Once.
|
|
63
|
+
*/
|
|
64
|
+
export function getServerIdentity() {
|
|
65
|
+
processIdentity ??= resolveServerIdentity(process.env, EMBEDDED_PACKAGE_IDENTITY);
|
|
66
|
+
return processIdentity;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The icons a host reads. With an HTTP base URL (the origin the server is reached at, ending in
|
|
70
|
+
* `/`), the icon is the URL src/httpApp.ts serves; without one (stdio), it travels inline.
|
|
71
|
+
*/
|
|
72
|
+
export function serverIcons(baseUrl) {
|
|
73
|
+
const src = baseUrl ? new URL(SERVER_ICON_PATH, baseUrl).href : serverIconDataUri();
|
|
74
|
+
return [{ src, mimeType: SERVER_ICON_MIME_TYPE, sizes: ["any"] }];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `serverInfo` (MCP `Implementation`) items beyond name and version, for a caller's locale.
|
|
78
|
+
* The factory writes `name` and `version` beside them from src/version.ts, where the
|
|
79
|
+
* version-observability check (D16) reads them.
|
|
80
|
+
*/
|
|
81
|
+
export function serverPresentation(identity, locale, baseUrl) {
|
|
82
|
+
return {
|
|
83
|
+
title: identity.title,
|
|
84
|
+
description: identity.description[locale],
|
|
85
|
+
websiteUrl: identity.websiteUrl,
|
|
86
|
+
icons: serverIcons(baseUrl),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The server instructions (`InitializeResult.instructions` / `DiscoverResult.instructions`): how
|
|
91
|
+
* an agent should use THIS server, critical guidance first, from the locale files
|
|
92
|
+
* (`Mcp.server.instructions`). A fork rewrites them there for its own tools;
|
|
93
|
+
* tests/serverMetadata.test.ts fails when a served tool goes unnamed or the text passes Claude
|
|
94
|
+
* Code's 2048-byte field budget.
|
|
95
|
+
*/
|
|
96
|
+
export function serverInstructions(locale) {
|
|
97
|
+
return t(locale, "server.instructions");
|
|
98
|
+
}
|
|
99
|
+
/** The Server Card `name` pattern (experimental-ext-server-card schema). */
|
|
100
|
+
const CARD_NAME = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
|
|
101
|
+
/**
|
|
102
|
+
* The card's reverse-DNS name, derived from the website: `io.github.<owner>/<name>` for a GitHub
|
|
103
|
+
* website (the MCP Registry convention), the reversed host otherwise (`www.` dropped).
|
|
104
|
+
*/
|
|
105
|
+
export function cardName(websiteUrl, serverName) {
|
|
106
|
+
const url = new URL(websiteUrl);
|
|
107
|
+
const host = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
108
|
+
const owner = url.pathname.split("/").filter(Boolean)[0];
|
|
109
|
+
const namespace = host === "github.com" && owner
|
|
110
|
+
? `io.github.${owner.toLowerCase()}`
|
|
111
|
+
: host.split(".").reverse().join(".");
|
|
112
|
+
const name = `${namespace}/${serverName}`;
|
|
113
|
+
if (!CARD_NAME.test(name))
|
|
114
|
+
throw new Error(`server card name ${JSON.stringify(name)} does not match ${CARD_NAME}`);
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
/** The card schema's `description` cap. */
|
|
118
|
+
export const CARD_DESCRIPTION_MAX = 100;
|
|
119
|
+
/**
|
|
120
|
+
* The card description: the English description whole when it fits the schema's 100 characters;
|
|
121
|
+
* otherwise cut on the last word boundary that leaves room for an ellipsis. Declared divergence:
|
|
122
|
+
* the card then carries a shortened text, the serverInfo the full one.
|
|
123
|
+
*/
|
|
124
|
+
export function cardDescription(description) {
|
|
125
|
+
if (description.length <= CARD_DESCRIPTION_MAX)
|
|
126
|
+
return description;
|
|
127
|
+
const room = description.slice(0, CARD_DESCRIPTION_MAX - 1);
|
|
128
|
+
const cut = room.lastIndexOf(" ");
|
|
129
|
+
return `${(cut > 0 ? room.slice(0, cut) : room).trimEnd()}…`;
|
|
130
|
+
}
|
package/dist/ui/uiResource.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export declare function buildItemsUiResourceMeta(domain?: string): McpUiResource
|
|
|
21
21
|
* The widget template. It carries no row: the widget renders the `list_items` RESULT it is given,
|
|
22
22
|
* so the template is the same for every caller and every page.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* What the widget shows, for the host and the model, in both languages ("EN / FR"). Hosts read it
|
|
26
|
+
* under two keys: `_meta.ui.widgetDescription` and the ChatGPT hint `_meta["openai/widgetDescription"]`
|
|
27
|
+
* (Manufact's tool-resource-metadata-complete reads exactly these); the same text under both.
|
|
28
|
+
*/
|
|
29
|
+
export declare const WIDGET_DESCRIPTION: string;
|
|
24
30
|
export declare function buildUiResource(props: {
|
|
25
31
|
domain?: string;
|
|
26
32
|
}): {
|
|
@@ -28,7 +34,14 @@ export declare function buildUiResource(props: {
|
|
|
28
34
|
mimeType: string;
|
|
29
35
|
text: string;
|
|
30
36
|
_meta: {
|
|
31
|
-
ui:
|
|
37
|
+
ui: {
|
|
38
|
+
widgetDescription: string;
|
|
39
|
+
csp?: import("@modelcontextprotocol/ext-apps").McpUiResourceCsp;
|
|
40
|
+
permissions?: import("@modelcontextprotocol/ext-apps").McpUiResourcePermissions;
|
|
41
|
+
domain?: string;
|
|
42
|
+
prefersBorder?: boolean;
|
|
43
|
+
};
|
|
44
|
+
"openai/widgetDescription": string;
|
|
32
45
|
};
|
|
33
46
|
};
|
|
34
47
|
/**
|
package/dist/ui/uiResource.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
16
|
+
import { tBoth } from "../i18n.js";
|
|
16
17
|
export const ITEMS_UI_RESOURCE_URI = "ui://mcp-boilerplate/items-widget.html";
|
|
17
18
|
// dist/ui-bundle/ — NEVER dist/ui/ (that path is tsc's compiled-server
|
|
18
19
|
// output for this very module; see vite.config.ts for the collision this
|
|
@@ -69,12 +70,21 @@ export function buildItemsUiResourceMeta(domain) {
|
|
|
69
70
|
* The widget template. It carries no row: the widget renders the `list_items` RESULT it is given,
|
|
70
71
|
* so the template is the same for every caller and every page.
|
|
71
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* What the widget shows, for the host and the model, in both languages ("EN / FR"). Hosts read it
|
|
75
|
+
* under two keys: `_meta.ui.widgetDescription` and the ChatGPT hint `_meta["openai/widgetDescription"]`
|
|
76
|
+
* (Manufact's tool-resource-metadata-complete reads exactly these); the same text under both.
|
|
77
|
+
*/
|
|
78
|
+
export const WIDGET_DESCRIPTION = tBoth("widget.description");
|
|
72
79
|
export function buildUiResource(props) {
|
|
73
80
|
return {
|
|
74
81
|
uri: ITEMS_UI_RESOURCE_URI,
|
|
75
82
|
mimeType: RESOURCE_MIME_TYPE,
|
|
76
83
|
text: readUiBundle(),
|
|
77
|
-
_meta: {
|
|
84
|
+
_meta: {
|
|
85
|
+
ui: { ...buildItemsUiResourceMeta(props.domain), widgetDescription: WIDGET_DESCRIPTION },
|
|
86
|
+
"openai/widgetDescription": WIDGET_DESCRIPTION,
|
|
87
|
+
},
|
|
78
88
|
};
|
|
79
89
|
}
|
|
80
90
|
/**
|
package/dist/version.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The advertised server version is DERIVED from package.json (never a literal).
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The advertised server version is DERIVED from package.json (never a literal): scripts/embed-static.mjs writes
|
|
3
|
+
* it into src/embedded.generated.ts at build, and `--check` (tests/embedded-static.test.ts) refuses a stale copy.
|
|
4
|
+
* It is a module value, not a file read at import: a bundled consumer carries no package.json beside this module
|
|
5
|
+
* (task k17aj33ks3ndyxgt7jch68rpwx8hz9x5).
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
7
|
+
import { EMBEDDED_PACKAGE_VERSION } from "./embedded.generated.js";
|
|
7
8
|
export const SERVER_NAME = "mcp-boilerplate";
|
|
8
|
-
export const SERVER_VERSION =
|
|
9
|
+
export const SERVER_VERSION = EMBEDDED_PACKAGE_VERSION;
|