@se-studio/site-mcp 0.1.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/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @se-studio/site-mcp
2
+
3
+ Public, **read-only**, **stateless** MCP for SE Studio marketing sites.
4
+
5
+ Agents use tools instead of scraping HTML:
6
+
7
+ | Tool | Purpose |
8
+ |------|---------|
9
+ | `search_site` | Hybrid search (injected; typically `@se-studio/search`) |
10
+ | `get_markdown` | Fetch `/{path}.md` export |
11
+ | `get_site_info` | Fetch `/site-info.md` |
12
+
13
+ This is **not** cms-edit. No OAuth, no draft writes. Runs on the marketing origin.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @se-studio/site-mcp
19
+ ```
20
+
21
+ ## Endpoint path
22
+
23
+ The **Next.js route file path** is the mount point (e.g. `app/api/mcp/route.ts` → `/api/mcp`).
24
+ `createSiteMcpHandler` does **not** take a path — routing is owned by App Router.
25
+
26
+ Use `endpointPath` only on **`buildMcpServerCard`** so the well-known card advertises the same URL agents should call:
27
+
28
+ | Site shape | Example `endpointPath` (server card) |
29
+ |------------|--------------------------------------|
30
+ | Standard SE Studio | `/api/mcp` |
31
+ | Top-level | `/mcp` |
32
+ | Point.me-style API prefix | `/marketing-api/mcp` |
33
+
34
+ When under `/api/`, update robots with `allowExtra` from `@se-studio/core-ui`:
35
+
36
+ ```ts
37
+ createRobotsTxtResponse({
38
+ isProduction,
39
+ baseUrl,
40
+ allowExtra: ['/api/mcp', '/api/search'],
41
+ });
42
+ ```
43
+
44
+ Some crawlers treat `Allow` loosely; discovery is primarily via `Link: rel="mcp-server-card"` and the well-known card.
45
+
46
+ ## Usage
47
+
48
+ ```ts
49
+ // app/api/mcp/route.ts
50
+ import { createSiteMcpHandler } from '@se-studio/site-mcp';
51
+ import { baseUrl } from '@/lib/server-config';
52
+
53
+ const handler = createSiteMcpHandler({
54
+ identity: {
55
+ name: 'se-studio',
56
+ title: 'Something Else',
57
+ websiteUrl: baseUrl,
58
+ },
59
+ tools: {
60
+ getMarkdown: {},
61
+ getSiteInfo: {},
62
+ // search: { search: async ({ query, limit, type }) => { … } },
63
+ },
64
+ });
65
+
66
+ export const GET = handler;
67
+ export const POST = handler;
68
+ export const DELETE = handler;
69
+ ```
70
+
71
+ ### Server card
72
+
73
+ ```ts
74
+ // app/.well-known/mcp/server-card.json/route.ts
75
+ import { buildMcpServerCard } from '@se-studio/site-mcp';
76
+ import { baseUrl } from '@/lib/server-config';
77
+
78
+ export function GET() {
79
+ const card = buildMcpServerCard({
80
+ identity: {
81
+ name: 'se-studio',
82
+ title: 'Something Else',
83
+ websiteUrl: baseUrl,
84
+ },
85
+ endpointPath: '/api/mcp', // must match the App Router mount
86
+ tools: { getMarkdown: {}, getSiteInfo: {} },
87
+ });
88
+ return Response.json(card, {
89
+ headers: {
90
+ 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate',
91
+ },
92
+ });
93
+ }
94
+ ```
95
+
96
+ ### Discovery Link header
97
+
98
+ ```ts
99
+ appendAgentDiscoveryLinkHeaders(response.headers, pathname, { mcpServerCard: true });
100
+ ```
101
+
102
+ ## Path mapping
103
+
104
+ Markdown paths use **`@se-studio/core-ui/agent-ready`** (`htmlPathToMarkdownPath`) — the same Band A helper as middleware content negotiation — so rules do not drift.
105
+
106
+ ## Safety defaults
107
+
108
+ - Fetch timeout: 10s (override `fetchTimeoutMs`)
109
+ - Max body: 512 KiB (override `maxBodyBytes`)
110
+ - Encoded path traversal rejected
111
+ - Tool failures set MCP `isError: true`
112
+
113
+ ## LLM reference
114
+
115
+ See [docs/llms.md](./docs/llms.md).
@@ -0,0 +1,8 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { SiteMcpHandlerConfig } from './types.js';
3
+ /**
4
+ * Create an MCP server instance with tools based on config.
5
+ * Fresh per request (stateless serverless).
6
+ */
7
+ export declare function createSiteMcpServer(config: SiteMcpHandlerConfig): McpServer;
8
+ //# sourceMappingURL=create-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAgBvD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,CAkH3E"}
@@ -0,0 +1,110 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { fetchTextLimited } from './fetch-text.js';
4
+ import { joinUrl, resolveMarkdownRequestPath } from './path.js';
5
+ import { listEnabledToolNames } from './server-card.js';
6
+ import { SITE_MCP_VERSION } from './version.js';
7
+ function toolText(text) {
8
+ return {
9
+ content: [{ type: 'text', text }],
10
+ };
11
+ }
12
+ function toolError(message) {
13
+ return {
14
+ content: [{ type: 'text', text: message }],
15
+ isError: true,
16
+ };
17
+ }
18
+ /**
19
+ * Create an MCP server instance with tools based on config.
20
+ * Fresh per request (stateless serverless).
21
+ */
22
+ export function createSiteMcpServer(config) {
23
+ const version = config.identity.version ?? SITE_MCP_VERSION;
24
+ const websiteUrl = config.identity.websiteUrl.replace(/\/+$/, '');
25
+ const fetchImpl = config.fetch ?? fetch;
26
+ const tools = config.tools ?? {};
27
+ const fetchTimeoutMs = config.fetchTimeoutMs;
28
+ const maxBodyBytes = config.maxBodyBytes;
29
+ const server = new McpServer({
30
+ name: config.identity.name,
31
+ version,
32
+ });
33
+ const enabled = listEnabledToolNames(tools);
34
+ if (enabled.includes('get_markdown')) {
35
+ server.tool('get_markdown', `Fetch a page or article from ${config.identity.title} as clean Markdown (frontmatter + body). ` +
36
+ 'Use public paths like "/", "/about", or article paths (not .md suffix). Prefer this over scraping HTML.', {
37
+ path: z.string().describe('Public page path, e.g. "/", "/about", "/work/case-study"'),
38
+ }, async ({ path }) => {
39
+ const mdPath = resolveMarkdownRequestPath(path);
40
+ if (!mdPath) {
41
+ return toolError(`Invalid or disallowed path: ${path}`);
42
+ }
43
+ const url = joinUrl(websiteUrl, mdPath);
44
+ const result = await fetchTextLimited(url, {
45
+ fetchImpl,
46
+ timeoutMs: fetchTimeoutMs,
47
+ maxBytes: maxBodyBytes,
48
+ headers: { Accept: 'text/markdown, text/plain, */*' },
49
+ });
50
+ if (!result.ok) {
51
+ return toolError(`${result.error} (${url})`);
52
+ }
53
+ // Raw markdown on success — agents can use it directly
54
+ return toolText(result.text);
55
+ });
56
+ }
57
+ if (enabled.includes('get_site_info')) {
58
+ server.tool('get_site_info', `Fetch the factual site-info document for ${config.identity.title} (counts, lists, summary for assistants).`, {}, async () => {
59
+ const url = joinUrl(websiteUrl, '/site-info.md');
60
+ const result = await fetchTextLimited(url, {
61
+ fetchImpl,
62
+ timeoutMs: fetchTimeoutMs,
63
+ maxBytes: maxBodyBytes,
64
+ headers: { Accept: 'text/markdown, text/plain, */*' },
65
+ });
66
+ if (!result.ok) {
67
+ return toolError(`${result.error} (${url})`);
68
+ }
69
+ return toolText(result.text);
70
+ });
71
+ }
72
+ if (enabled.includes('search_site') && tools.search) {
73
+ const searchFn = tools.search.search;
74
+ server.tool('search_site', `Search published content on ${config.identity.title}. Returns titles, hrefs, and short snippets. ` +
75
+ 'Use get_markdown on a result href/path for full page content.', {
76
+ query: z.string().min(1).describe('Search query'),
77
+ limit: z
78
+ .number()
79
+ .int()
80
+ .min(1)
81
+ .max(20)
82
+ .optional()
83
+ .describe('Max results (1–20, default 10)'),
84
+ type: z
85
+ .string()
86
+ .optional()
87
+ .describe('Optional content type filter, e.g. page, article, person'),
88
+ }, async ({ query, limit, type }) => {
89
+ const capped = Math.min(limit ?? 10, 20);
90
+ try {
91
+ const result = await searchFn({
92
+ query: query.trim(),
93
+ limit: capped,
94
+ type,
95
+ });
96
+ return toolText(JSON.stringify({
97
+ query: result.query,
98
+ totalCount: result.totalCount,
99
+ results: result.results,
100
+ }));
101
+ }
102
+ catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ return toolError(`Search failed: ${message}`);
105
+ }
106
+ });
107
+ }
108
+ return server;
109
+ }
110
+ //# sourceMappingURL=create-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-server.js","sourceRoot":"","sources":["../src/create-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /** Default timeout for origin fetches (markdown / site-info). */
2
+ export declare const DEFAULT_FETCH_TIMEOUT_MS = 10000;
3
+ /** Default max body size (~512 KiB) for public tool responses. */
4
+ export declare const DEFAULT_MAX_BODY_BYTES: number;
5
+ export type FetchTextResult = {
6
+ ok: true;
7
+ text: string;
8
+ status: number;
9
+ } | {
10
+ ok: false;
11
+ error: string;
12
+ status?: number;
13
+ };
14
+ /**
15
+ * Fetch a URL as text with timeout and max body size.
16
+ */
17
+ export declare function fetchTextLimited(url: string, options?: {
18
+ fetchImpl?: typeof fetch;
19
+ timeoutMs?: number;
20
+ maxBytes?: number;
21
+ headers?: Record<string, string>;
22
+ }): Promise<FetchTextResult>;
23
+ //# sourceMappingURL=fetch-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-text.d.ts","sourceRoot":"","sources":["../src/fetch-text.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,kEAAkE;AAClE,eAAO,MAAM,sBAAsB,QAAa,CAAC;AAEjD,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B,GACL,OAAO,CAAC,eAAe,CAAC,CAmD1B"}
@@ -0,0 +1,56 @@
1
+ /** Default timeout for origin fetches (markdown / site-info). */
2
+ export const DEFAULT_FETCH_TIMEOUT_MS = 10_000;
3
+ /** Default max body size (~512 KiB) for public tool responses. */
4
+ export const DEFAULT_MAX_BODY_BYTES = 512 * 1024;
5
+ /**
6
+ * Fetch a URL as text with timeout and max body size.
7
+ */
8
+ export async function fetchTextLimited(url, options = {}) {
9
+ const fetchImpl = options.fetchImpl ?? fetch;
10
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
11
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BODY_BYTES;
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
14
+ try {
15
+ const res = await fetchImpl(url, {
16
+ headers: options.headers,
17
+ signal: controller.signal,
18
+ });
19
+ if (!res.ok) {
20
+ return { ok: false, error: `HTTP ${res.status}`, status: res.status };
21
+ }
22
+ const contentLength = res.headers.get('content-length');
23
+ if (contentLength) {
24
+ const len = Number(contentLength);
25
+ if (Number.isFinite(len) && len > maxBytes) {
26
+ return {
27
+ ok: false,
28
+ error: `Content too large (${len} bytes; max ${maxBytes})`,
29
+ status: res.status,
30
+ };
31
+ }
32
+ }
33
+ // Prefer arrayBuffer so we can enforce a hard cap without loading unbounded strings
34
+ const buffer = await res.arrayBuffer();
35
+ if (buffer.byteLength > maxBytes) {
36
+ return {
37
+ ok: false,
38
+ error: `Content too large (${buffer.byteLength} bytes; max ${maxBytes})`,
39
+ status: res.status,
40
+ };
41
+ }
42
+ const text = new TextDecoder('utf-8').decode(buffer);
43
+ return { ok: true, text, status: res.status };
44
+ }
45
+ catch (err) {
46
+ if (err instanceof Error && err.name === 'AbortError') {
47
+ return { ok: false, error: `Request timed out after ${timeoutMs}ms` };
48
+ }
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ return { ok: false, error: message };
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ //# sourceMappingURL=fetch-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-text.js","sourceRoot":"","sources":["../src/fetch-text.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAE/C,kEAAkE;AAClE,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,GAAG,IAAI,CAAC;AAMjD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,UAKI,EAAE;IAEN,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,wBAAwB,CAAC;IAChE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,sBAAsB,CAAC;IAE5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YAC/B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;QACxE,CAAC;QAED,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACxD,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,GAAG,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;YAClC,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;gBAC3C,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE,sBAAsB,GAAG,eAAe,QAAQ,GAAG;oBAC1D,MAAM,EAAE,GAAG,CAAC,MAAM;iBACnB,CAAC;YACJ,CAAC;QACH,CAAC;QAED,oFAAoF;QACpF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,MAAM,CAAC,UAAU,GAAG,QAAQ,EAAE,CAAC;YACjC,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,sBAAsB,MAAM,CAAC,UAAU,eAAe,QAAQ,GAAG;gBACxE,MAAM,EAAE,GAAG,CAAC,MAAM;aACnB,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAChD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACtD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,SAAS,IAAI,EAAE,CAAC;QACxE,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACvC,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const SITE_MCP_PACKAGE_NAME = "@se-studio/site-mcp";
2
+ export declare const SITE_MCP_VERSION = "0.1.0";
3
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/generated/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAC3D,eAAO,MAAM,gBAAgB,UAAU,CAAC"}
@@ -0,0 +1,4 @@
1
+ // Generated by scripts/generate-version.mjs — do not edit manually.
2
+ export const SITE_MCP_PACKAGE_NAME = "@se-studio/site-mcp";
3
+ export const SITE_MCP_VERSION = "0.1.0";
4
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/generated/version.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { SiteMcpHandlerConfig } from './types.js';
2
+ /**
3
+ * Create a Next.js-compatible route handler for public site MCP (stateless Streamable HTTP).
4
+ *
5
+ * Mount at the route that matches your public path (e.g. `app/api/mcp/route.ts` → `/api/mcp`).
6
+ * The handler does not take `endpointPath` — that value is only for {@link buildMcpServerCard}
7
+ * so the well-known card advertises the same URL agents should call.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // app/api/mcp/route.ts
12
+ * import { createSiteMcpHandler } from '@se-studio/site-mcp';
13
+ * const handler = createSiteMcpHandler({ identity: {…}, tools: {…} });
14
+ * export const GET = handler;
15
+ * export const POST = handler;
16
+ * export const DELETE = handler;
17
+ * ```
18
+ */
19
+ export declare function createSiteMcpHandler(config: SiteMcpHandlerConfig): (request: Request) => Promise<Response>;
20
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAsBvD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,GAC3B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAqBzC"}
@@ -0,0 +1,58 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
2
+ import { createSiteMcpServer } from './create-server.js';
3
+ const CORS_HEADERS = {
4
+ 'Access-Control-Allow-Origin': '*',
5
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
6
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method, Mcp-Name',
7
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id, Mcp-Protocol-Version',
8
+ };
9
+ function withCors(response) {
10
+ const headers = new Headers(response.headers);
11
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
12
+ headers.set(key, value);
13
+ }
14
+ return new Response(response.body, {
15
+ status: response.status,
16
+ statusText: response.statusText,
17
+ headers,
18
+ });
19
+ }
20
+ /**
21
+ * Create a Next.js-compatible route handler for public site MCP (stateless Streamable HTTP).
22
+ *
23
+ * Mount at the route that matches your public path (e.g. `app/api/mcp/route.ts` → `/api/mcp`).
24
+ * The handler does not take `endpointPath` — that value is only for {@link buildMcpServerCard}
25
+ * so the well-known card advertises the same URL agents should call.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // app/api/mcp/route.ts
30
+ * import { createSiteMcpHandler } from '@se-studio/site-mcp';
31
+ * const handler = createSiteMcpHandler({ identity: {…}, tools: {…} });
32
+ * export const GET = handler;
33
+ * export const POST = handler;
34
+ * export const DELETE = handler;
35
+ * ```
36
+ */
37
+ export function createSiteMcpHandler(config) {
38
+ return async function siteMcpHandler(request) {
39
+ if (request.method === 'OPTIONS') {
40
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
41
+ }
42
+ try {
43
+ const server = createSiteMcpServer(config);
44
+ const transport = new WebStandardStreamableHTTPServerTransport({
45
+ sessionIdGenerator: undefined,
46
+ enableJsonResponse: true,
47
+ });
48
+ await server.connect(transport);
49
+ const response = await transport.handleRequest(request);
50
+ return withCors(response);
51
+ }
52
+ catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ return withCors(Response.json({ error: 'MCP handler failed', message }, { status: 500 }));
55
+ }
56
+ };
57
+ }
58
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wCAAwC,EAAE,MAAM,+DAA+D,CAAC;AACzH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,MAAM,YAAY,GAA2B;IAC3C,6BAA6B,EAAE,GAAG;IAClC,8BAA8B,EAAE,4BAA4B;IAC5D,8BAA8B,EAC5B,iGAAiG;IACnG,+BAA+B,EAAE,sCAAsC;CACxE,CAAC;AAEF,SAAS,QAAQ,CAAC,QAAkB;IAClC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;QACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO;KACR,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA4B;IAE5B,OAAO,KAAK,UAAU,cAAc,CAAC,OAAgB;QACnD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,SAAS,GAAG,IAAI,wCAAwC,CAAC;gBAC7D,kBAAkB,EAAE,SAAS;gBAC7B,kBAAkB,EAAE,IAAI;aACzB,CAAC,CAAC;YAEH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACxD,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,OAAO,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { createSiteMcpServer } from './create-server.js';
2
+ export type { FetchTextResult } from './fetch-text.js';
3
+ export { DEFAULT_FETCH_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES, fetchTextLimited, } from './fetch-text.js';
4
+ export { createSiteMcpHandler } from './handler.js';
5
+ export { htmlPathToMarkdownPath, joinUrl, MAX_MARKDOWN_PATH_INPUT, normalizeEndpointPath, resolveMarkdownRequestPath, } from './path.js';
6
+ export { buildMcpServerCard, listEnabledToolNames } from './server-card.js';
7
+ export type { McpServerCard, SiteMcpHandlerConfig, SiteMcpIdentity, SiteMcpSearchArgs, SiteMcpSearchFn, SiteMcpSearchHit, SiteMcpToolsConfig, } from './types.js';
8
+ export { DEFAULT_ENDPOINT_PATH } from './types.js';
9
+ export { SITE_MCP_PACKAGE_NAME, SITE_MCP_VERSION } from './version.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EACL,sBAAsB,EACtB,OAAO,EACP,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createSiteMcpServer } from './create-server.js';
2
+ export { DEFAULT_FETCH_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES, fetchTextLimited, } from './fetch-text.js';
3
+ export { createSiteMcpHandler } from './handler.js';
4
+ export { htmlPathToMarkdownPath, joinUrl, MAX_MARKDOWN_PATH_INPUT, normalizeEndpointPath, resolveMarkdownRequestPath, } from './path.js';
5
+ export { buildMcpServerCard, listEnabledToolNames } from './server-card.js';
6
+ export { DEFAULT_ENDPOINT_PATH } from './types.js';
7
+ export { SITE_MCP_PACKAGE_NAME, SITE_MCP_VERSION } from './version.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EACL,sBAAsB,EACtB,OAAO,EACP,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAU5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/dist/path.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Site-mcp path helpers.
3
+ *
4
+ * HTML → markdown mapping lives in `@se-studio/core-ui/agent-ready` (Band A) so
5
+ * middleware negotiation and this package stay in sync. This module only adds
6
+ * agent-input normalization and URL joining.
7
+ */
8
+ import { htmlPathToMarkdownPath } from '@se-studio/core-ui/agent-ready';
9
+ export { htmlPathToMarkdownPath };
10
+ /** Cap agent-provided path strings before mapping. */
11
+ export declare const MAX_MARKDOWN_PATH_INPUT = 512;
12
+ /**
13
+ * Normalize endpoint path: leading slash, no trailing slash (except root).
14
+ * Used by the server card so agents know the transport URL; the Next.js route
15
+ * path is the mount point (they must match).
16
+ */
17
+ export declare function normalizeEndpointPath(path: string | undefined): string;
18
+ /**
19
+ * Normalize agent-provided path input for get_markdown.
20
+ */
21
+ export declare function resolveMarkdownRequestPath(input: string): string | null;
22
+ /**
23
+ * Join origin + path without double slashes.
24
+ */
25
+ export declare function joinUrl(origin: string, path: string): string;
26
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAOtE;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOvE;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI5D"}
package/dist/path.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Site-mcp path helpers.
3
+ *
4
+ * HTML → markdown mapping lives in `@se-studio/core-ui/agent-ready` (Band A) so
5
+ * middleware negotiation and this package stay in sync. This module only adds
6
+ * agent-input normalization and URL joining.
7
+ */
8
+ import { htmlPathToMarkdownPath } from '@se-studio/core-ui/agent-ready';
9
+ export { htmlPathToMarkdownPath };
10
+ /** Cap agent-provided path strings before mapping. */
11
+ export const MAX_MARKDOWN_PATH_INPUT = 512;
12
+ /**
13
+ * Normalize endpoint path: leading slash, no trailing slash (except root).
14
+ * Used by the server card so agents know the transport URL; the Next.js route
15
+ * path is the mount point (they must match).
16
+ */
17
+ export function normalizeEndpointPath(path) {
18
+ const raw = (path ?? '/api/mcp').trim() || '/api/mcp';
19
+ let p = raw.startsWith('/') ? raw : `/${raw}`;
20
+ if (p.length > 1 && p.endsWith('/')) {
21
+ p = p.slice(0, -1);
22
+ }
23
+ return p;
24
+ }
25
+ /**
26
+ * Normalize agent-provided path input for get_markdown.
27
+ */
28
+ export function resolveMarkdownRequestPath(input) {
29
+ const trimmed = input.trim();
30
+ if (!trimmed || trimmed.length > MAX_MARKDOWN_PATH_INPUT) {
31
+ return null;
32
+ }
33
+ const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
34
+ return htmlPathToMarkdownPath(withSlash);
35
+ }
36
+ /**
37
+ * Join origin + path without double slashes.
38
+ */
39
+ export function joinUrl(origin, path) {
40
+ const base = origin.replace(/\/+$/, '');
41
+ const p = path.startsWith('/') ? path : `/${path}`;
42
+ return `${base}${p}`;
43
+ }
44
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,sDAAsD;AACtD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAwB;IAC5D,MAAM,GAAG,GAAG,CAAC,IAAI,IAAI,UAAU,CAAC,CAAC,IAAI,EAAE,IAAI,UAAU,CAAC;IACtD,IAAI,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IAC9C,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACpC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IACpE,OAAO,sBAAsB,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,IAAY;IAClD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACxC,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACnD,OAAO,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC;AACvB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { type McpServerCard, type SiteMcpIdentity, type SiteMcpToolsConfig } from './types.js';
2
+ export declare function listEnabledToolNames(tools?: SiteMcpToolsConfig): string[];
3
+ /**
4
+ * Build a static-ish MCP server card for `/.well-known/mcp/server-card.json`.
5
+ *
6
+ * `endpointPath` is advertisement-only: set it to the same path as your Next.js
7
+ * route (e.g. `/api/mcp` for `app/api/mcp/route.ts`). The HTTP handler does not
8
+ * read this value — the App Router mount is the source of truth for routing.
9
+ */
10
+ export declare function buildMcpServerCard(options: {
11
+ identity: SiteMcpIdentity;
12
+ endpointPath?: string;
13
+ tools?: SiteMcpToolsConfig | string[];
14
+ }): McpServerCard;
15
+ //# sourceMappingURL=server-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-card.d.ts","sourceRoot":"","sources":["../src/server-card.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAGpB,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAYzE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,kBAAkB,GAAG,MAAM,EAAE,CAAC;CACvC,GAAG,aAAa,CAkBhB"}
@@ -0,0 +1,42 @@
1
+ import { joinUrl, normalizeEndpointPath } from './path.js';
2
+ import { DEFAULT_ENDPOINT_PATH, } from './types.js';
3
+ import { SITE_MCP_VERSION } from './version.js';
4
+ export function listEnabledToolNames(tools) {
5
+ const names = [];
6
+ if (tools?.getMarkdown?.enabled !== false && tools?.getMarkdown) {
7
+ names.push('get_markdown');
8
+ }
9
+ if (tools?.getSiteInfo?.enabled !== false && tools?.getSiteInfo) {
10
+ names.push('get_site_info');
11
+ }
12
+ if (tools?.search) {
13
+ names.push('search_site');
14
+ }
15
+ return names;
16
+ }
17
+ /**
18
+ * Build a static-ish MCP server card for `/.well-known/mcp/server-card.json`.
19
+ *
20
+ * `endpointPath` is advertisement-only: set it to the same path as your Next.js
21
+ * route (e.g. `/api/mcp` for `app/api/mcp/route.ts`). The HTTP handler does not
22
+ * read this value — the App Router mount is the source of truth for routing.
23
+ */
24
+ export function buildMcpServerCard(options) {
25
+ const endpointPath = normalizeEndpointPath(options.endpointPath ?? DEFAULT_ENDPOINT_PATH);
26
+ const websiteUrl = options.identity.websiteUrl.replace(/\/+$/, '');
27
+ const toolNames = Array.isArray(options.tools)
28
+ ? options.tools
29
+ : listEnabledToolNames(options.tools);
30
+ return {
31
+ name: options.identity.name,
32
+ title: options.identity.title,
33
+ version: options.identity.version ?? SITE_MCP_VERSION,
34
+ websiteUrl,
35
+ endpointPath,
36
+ endpoint: joinUrl(websiteUrl, endpointPath),
37
+ transport: 'streamable-http',
38
+ tools: toolNames,
39
+ description: `Public read-only MCP for ${options.identity.title}: query site content without scraping HTML.`,
40
+ };
41
+ }
42
+ //# sourceMappingURL=server-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-card.js","sourceRoot":"","sources":["../src/server-card.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EACL,qBAAqB,GAItB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,UAAU,oBAAoB,CAAC,KAA0B;IAC7D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,EAAE,WAAW,EAAE,OAAO,KAAK,KAAK,IAAI,KAAK,EAAE,WAAW,EAAE,CAAC;QAChE,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,EAAE,WAAW,EAAE,OAAO,KAAK,KAAK,IAAI,KAAK,EAAE,WAAW,EAAE,CAAC;QAChE,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC9B,CAAC;IACD,IAAI,KAAK,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAIlC;IACC,MAAM,YAAY,GAAG,qBAAqB,CAAC,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC,CAAC;IAC1F,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;QAC5C,CAAC,CAAC,OAAO,CAAC,KAAK;QACf,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAExC,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;QAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK;QAC7B,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,IAAI,gBAAgB;QACrD,UAAU;QACV,YAAY;QACZ,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC;QAC3C,SAAS,EAAE,iBAAiB;QAC5B,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,4BAA4B,OAAO,CAAC,QAAQ,CAAC,KAAK,6CAA6C;KAC7G,CAAC;AACJ,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Identity for the public site MCP server and server-card.
3
+ */
4
+ export interface SiteMcpIdentity {
5
+ /** Stable MCP server name (e.g. `se-studio`). */
6
+ name: string;
7
+ /** Human title (e.g. `Something Else`). */
8
+ title: string;
9
+ /** Absolute origin, e.g. `https://www.se.studio` (no trailing slash preferred). */
10
+ websiteUrl: string;
11
+ /** Optional override; defaults to package version from package.json. */
12
+ version?: string;
13
+ }
14
+ /**
15
+ * Compact search hit returned by the injected search function.
16
+ */
17
+ export interface SiteMcpSearchHit {
18
+ title: string;
19
+ description?: string;
20
+ href: string;
21
+ type?: string;
22
+ score?: number;
23
+ /** Short body snippet (keep small for agent context). */
24
+ snippet?: string;
25
+ }
26
+ export interface SiteMcpSearchArgs {
27
+ query: string;
28
+ limit: number;
29
+ type?: string;
30
+ }
31
+ export type SiteMcpSearchFn = (args: SiteMcpSearchArgs) => Promise<{
32
+ results: SiteMcpSearchHit[];
33
+ query: string;
34
+ totalCount: number;
35
+ }>;
36
+ export interface SiteMcpToolsConfig {
37
+ /** Enable get_markdown (self-fetch of .md exports). Present object enables the tool. */
38
+ getMarkdown?: {
39
+ enabled?: boolean;
40
+ };
41
+ /** Enable get_site_info (self-fetch /site-info.md). */
42
+ getSiteInfo?: {
43
+ enabled?: boolean;
44
+ };
45
+ /** Enable search_site via injected function (no hard dep on @se-studio/search). */
46
+ search?: {
47
+ search: SiteMcpSearchFn;
48
+ };
49
+ }
50
+ /**
51
+ * Config for the HTTP MCP handler (mounted at whatever Next.js route path you choose).
52
+ *
53
+ * `endpointPath` is **not** needed here — the App Router file path is the mount point.
54
+ * Pass the same path to {@link buildMcpServerCard} so the server-card advertises it.
55
+ */
56
+ export interface SiteMcpHandlerConfig {
57
+ identity: SiteMcpIdentity;
58
+ tools?: SiteMcpToolsConfig;
59
+ /** Optional fetch implementation (tests). Defaults to global fetch. */
60
+ fetch?: typeof fetch;
61
+ /** Origin fetch timeout in ms (default 10s). */
62
+ fetchTimeoutMs?: number;
63
+ /** Max markdown/site-info body bytes (default 512 KiB). */
64
+ maxBodyBytes?: number;
65
+ }
66
+ export interface McpServerCard {
67
+ name: string;
68
+ title: string;
69
+ version: string;
70
+ websiteUrl: string;
71
+ /** Absolute MCP endpoint URL. */
72
+ endpoint: string;
73
+ endpointPath: string;
74
+ transport: 'streamable-http';
75
+ tools: string[];
76
+ description: string;
77
+ }
78
+ export declare const DEFAULT_ENDPOINT_PATH = "/api/mcp";
79
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC;IACjE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,WAAW,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,uDAAuD;IACvD,WAAW,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,mFAAmF;IACnF,MAAM,CAAC,EAAE;QACP,MAAM,EAAE,eAAe,CAAC;KACzB,CAAC;CACH;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,gDAAgD;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,qBAAqB,aAAa,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_ENDPOINT_PATH = '/api/mcp';
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAgFA,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { SITE_MCP_PACKAGE_NAME, SITE_MCP_VERSION } from './generated/version.js';
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { SITE_MCP_PACKAGE_NAME, SITE_MCP_VERSION } from './generated/version.js';
2
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
package/docs/llms.md ADDED
@@ -0,0 +1,52 @@
1
+ # @se-studio/site-mcp — LLM reference
2
+
3
+ Public read-only MCP for marketing sites. Stateless Streamable HTTP. Not cms-edit.
4
+
5
+ ## Exports
6
+
7
+ | Export | Purpose |
8
+ |--------|---------|
9
+ | `createSiteMcpHandler(config)` | Next route handler (GET/POST/DELETE/OPTIONS) |
10
+ | `createSiteMcpServer(config)` | In-process `McpServer` (tests) |
11
+ | `buildMcpServerCard(options)` | JSON for `/.well-known/mcp/server-card.json` (`endpointPath` here) |
12
+ | `listEnabledToolNames(tools)` | Tool names from config |
13
+ | `normalizeEndpointPath` / `resolveMarkdownRequestPath` / `htmlPathToMarkdownPath` | Path helpers |
14
+ | `fetchTextLimited` | Timeout + size-capped fetch |
15
+ | `SITE_MCP_VERSION` | From `package.json` via generate-version |
16
+
17
+ ## Config
18
+
19
+ ```ts
20
+ // Handler — no endpointPath (App Router owns the mount)
21
+ createSiteMcpHandler({
22
+ identity: { name, title, websiteUrl, version? },
23
+ tools?: {
24
+ getMarkdown?: { enabled?: boolean },
25
+ getSiteInfo?: { enabled?: boolean },
26
+ search?: { search: (args) => Promise<{ results, query, totalCount }> },
27
+ },
28
+ fetch?: typeof fetch,
29
+ fetchTimeoutMs?: number, // default 10000
30
+ maxBodyBytes?: number, // default 512KiB
31
+ });
32
+
33
+ // Server card — advertise the same path as the route file
34
+ buildMcpServerCard({
35
+ identity,
36
+ endpointPath: '/api/mcp', // default
37
+ tools,
38
+ });
39
+ ```
40
+
41
+ ## Tools
42
+
43
+ - **get_markdown** `{ path }` → raw markdown text on success; `isError` on failure
44
+ - **get_site_info** → raw `/site-info.md` text
45
+ - **search_site** `{ query, limit?, type? }` → JSON hits; omit config to hide tool
46
+
47
+ ## Paths
48
+
49
+ - Mount via Next route (e.g. `/api/mcp`)
50
+ - Card `endpointPath` must match mount
51
+ - Markdown mapping: `@se-studio/core-ui/agent-ready` (shared with Band A middleware)
52
+ - Robots: `allowExtra: ['/api/mcp', '/api/search']` when under `Disallow: /api/`
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@se-studio/site-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Public, read-only, stateless MCP for SE Studio marketing sites (search, markdown, site-info)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Something-Else-Studio/se-core-product",
8
+ "directory": "packages/site-mcp"
9
+ },
10
+ "license": "MIT",
11
+ "engines": {
12
+ "node": ">=24.0.0 <25"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "*.md",
26
+ "docs"
27
+ ],
28
+ "scripts": {
29
+ "generate:version": "node scripts/generate-version.mjs",
30
+ "build": "node scripts/generate-version.mjs && tsc --project tsconfig.build.json",
31
+ "dev": "node scripts/generate-version.mjs && tsc --project tsconfig.build.json --watch",
32
+ "type-check": "node scripts/generate-version.mjs && tsc --noEmit",
33
+ "lint": "biome lint .",
34
+ "test": "node scripts/generate-version.mjs && vitest run",
35
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@se-studio/core-ui": "workspace:*",
40
+ "zod": "^4.4.3"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.5.5",
44
+ "@types/node": "^24.13.3",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^4.1.10"
47
+ }
48
+ }