--- title: "Rendez les connaissances de votre site disponibles via MCP" description: "Publiez des outils MCP en lecture seule qui recherchent dans votre documentation : Busymate AI y puise ses réponses, avec des liens vers vos pages, au lieu d’explorer votre site." last_updated: "2026-10-28T09:48:54+02:00" --- # Rendez les connaissances de votre site disponibles via MCP Source: https://busymate.ai/fr/docs/guides/site-mcp-knowledge Last modified: 2026-10-28T09:48:54+02:00 Expose documentation through an MCP server with source links. Busymate AI can answer through its tools without copying your site. Build, advertise and connect the server below. ## 1. Design the knowledge tools Use descriptive knowledge-tool names. Public delivery quotes, coverage lookups and account searches do not replace a knowledge base. | Tool | Input | Returns | |---|---|---| | `search_docs` | `query`, optional `limit` (1–10) | `results`: title, url, section, snippet | | `read_docs_page` | `url` from a search result | title, url, section, full `content` | | `list_docs_sections` | nothing | `sections`, each with its pages and URLs | | `get_setup_steps` (optional) | optional `platform` | ordered `steps`, each with a url | | `get_pricing` (optional) | nothing | your published plans and the pricing page url | | `get_faq` (optional) | optional `topic` | question, answer and url per entry | The full `tools/list` entries, with input and output schemas: ```json { "tools": [ { "name": "search_docs", "title": "Search the docs", "description": "Search the public documentation. Returns the best-matching pages with their URLs; cite the URL you use.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "minLength": 1, "maxLength": 200, "description": "What the visitor asked, in their words." }, "limit": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5 } }, "required": [ "query" ] }, "outputSchema": { "type": "object", "properties": { "results": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "section": { "type": "string" }, "snippet": { "type": "string" } }, "required": [ "title", "url", "snippet" ] } } }, "required": [ "results" ] }, "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "_meta": { "securitySchemes": [ { "type": "noauth" } ] } }, { "name": "read_docs_page", "title": "Read one docs page", "description": "Return the full text of one documentation page by its URL (from search_docs or list_docs_sections).", "inputSchema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } }, "required": [ "url" ] }, "outputSchema": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "section": { "type": "string" }, "content": { "type": "string" } }, "required": [ "title", "url", "content" ] }, "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "_meta": { "securitySchemes": [ { "type": "noauth" } ] } }, { "name": "list_docs_sections", "title": "List the docs sections", "description": "List the documentation's sections and the pages in each, with URLs.", "inputSchema": { "type": "object", "properties": {} }, "outputSchema": { "type": "object", "properties": { "sections": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "pages": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string", "format": "uri" } }, "required": [ "title", "url" ] } } }, "required": [ "name", "pages" ] } } }, "required": [ "sections" ] }, "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "_meta": { "securitySchemes": [ { "type": "noauth" } ] } }, { "name": "get_setup_steps", "title": "Get setup steps", "description": "Optional. Ordered setup steps for one platform, each with the page URL that explains it.", "inputSchema": { "type": "object", "properties": { "platform": { "type": "string", "enum": [ "all", "web", "ios", "android", "desktop" ], "default": "all" } } }, "outputSchema": { "type": "object", "properties": { "platform": { "type": "string" }, "steps": { "type": "array", "items": { "type": "object", "properties": { "step": { "type": "string" }, "url": { "type": "string", "format": "uri" } }, "required": [ "step", "url" ] } } }, "required": [ "platform", "steps" ] }, "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "_meta": { "securitySchemes": [ { "type": "noauth" } ] } } ] } ``` A `search_docs` call returns the data twice: as text for every client, and as `structuredContent` that matches the output schema: ```json { "content": [ { "type": "text", "text": "{\"results\":[{\"title\":\"Refunds\",\"url\":\"https://docs.example.com/billing/refunds\",\"section\":\"Billing\",\"snippet\":\"Refunds are issued within 14 days of purchase from Settings › Billing.\"}]}" } ], "structuredContent": { "results": [ { "title": "Refunds", "url": "https://docs.example.com/billing/refunds", "section": "Billing", "snippet": "Refunds are issued within 14 days of purchase from Settings › Billing." } ] } } ``` Accept anonymous `initialize` and `tools/list` requests. An OAuth-only endpoint returning `401` before listing tools cannot serve public knowledge; expose a separate endpoint if necessary. Four rules decide whether Busymate AI can offer a tool to anonymous visitors: 1. **Annotate it read-only.** Set `readOnlyHint: true` and `destructiveHint: false`. A tool without that annotation is never offered to visitors. 2. **Declare it public.** Add `securitySchemes: [{ "type": "noauth" }]` under `_meta`. This explicitly declares public access. A standard read-only tool without security metadata is offered as an anonymous candidate: access is established only by a real requested call. Explicit OAuth-only tools are excluded. Discovery does not execute tools with invented arguments. A 401, a 403 or a sign-in error never grants public access. 3. **Use a read name.** A name with a write verb (`create_`, `update_`, `delete_`, `send_` and similar) is never called, whatever its annotations say. 4. **Return absolute page URLs.** These are the citations visitors see. ## 2. Run the server Each sample serves Streamable HTTP at `/mcp`. Replace sample pages with your search. TypeScript, with the official MCP SDK: ```typescript // npm init -y && npm pkg set type=module && npm i @modelcontextprotocol/sdk@1.32.2 zod@4 // Run: node server.ts (Node 24), or npx tsx server.ts. Serves http://localhost:3000/mcp. import { createServer } from "node:http"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; type Page = { title: string; url: string; section: string; text: string }; // Replace with your own search: a database, a search service or a JSON file. const PAGES: Page[] = [ { title: "Getting started", url: "https://docs.example.com/start", section: "Basics", text: "Create an account, add your site, then publish." }, { title: "Refunds", url: "https://docs.example.com/billing/refunds", section: "Billing", text: "Refunds are issued within 14 days of purchase from Settings › Billing." }, { title: "Install on iPhone", url: "https://docs.example.com/setup/iphone", section: "Setup", text: "Install the app from the App Store and sign in with your work email." }, ]; const PUBLIC_READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }; const NO_AUTH = { securitySchemes: [{ type: "noauth" }] }; const reply = (data: Record) => ({ content: [{ type: "text" as const, text: JSON.stringify(data) }], structuredContent: data }); const link = z.object({ title: z.string(), url: z.string() }); function buildServer(): McpServer { const server = new McpServer({ name: "example-docs", version: "1.0.0" }); server.registerTool("search_docs", { title: "Search the docs", description: "Search the public documentation. Returns the best-matching pages with their URLs; cite the URL you use.", inputSchema: { query: z.string().min(1).max(200), limit: z.number().int().min(1).max(10).optional() }, outputSchema: { results: z.array(link.extend({ section: z.string(), snippet: z.string() })) }, annotations: PUBLIC_READ, _meta: NO_AUTH, }, async ({ query, limit = 5 }) => { const terms = query.toLowerCase().split(/\W+/).filter(Boolean); const results = PAGES .map((p) => ({ p, score: terms.filter((t) => `${p.title} ${p.text}`.toLowerCase().includes(t)).length })) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(({ p }) => ({ title: p.title, url: p.url, section: p.section, snippet: p.text.slice(0, 300) })); return reply({ results }); }); server.registerTool("read_docs_page", { title: "Read one docs page", description: "Return the full text of one documentation page by its URL (from search_docs or list_docs_sections).", inputSchema: { url: z.string() }, annotations: PUBLIC_READ, _meta: NO_AUTH, }, async ({ url }) => { const page = PAGES.find((p) => p.url === url); if (!page) return { isError: true, content: [{ type: "text" as const, text: `No page at ${url}. Use search_docs to find one.` }] }; return reply({ title: page.title, url: page.url, section: page.section, content: page.text }); }); server.registerTool("list_docs_sections", { title: "List the docs sections", description: "List the documentation's sections and the pages in each, with URLs.", inputSchema: {}, outputSchema: { sections: z.array(z.object({ name: z.string(), pages: z.array(link) })) }, annotations: PUBLIC_READ, _meta: NO_AUTH, }, async () => { const names = [...new Set(PAGES.map((p) => p.section))]; return reply({ sections: names.map((name) => ({ name, pages: PAGES.filter((p) => p.section === name).map(({ title, url }) => ({ title, url })) })) }); }); return server; } createServer(async (req, res) => { if (!req.url?.startsWith("/mcp")) return void res.writeHead(404).end(); if (req.method !== "POST") return void res.writeHead(405, { allow: "POST" }).end(); // Stateless: a fresh server per request, so nothing is shared between visitors. const server = buildServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on("close", () => void transport.close().then(() => server.close())); await server.connect(transport); await transport.handleRequest(req, res); }).listen(Number(process.env.PORT ?? 3000)); ``` Python, with FastMCP: ```python # pip install fastmcp==4.1.4 — run: python server.py (Python 3.10+) from typing import Annotated from fastmcp import FastMCP from fastmcp.exceptions import ToolError from pydantic import Field # Replace with your own search: a database, a search service or a JSON file. PAGES = [ {"title": "Getting started", "url": "https://docs.example.com/start", "section": "Basics", "text": "Create an account, add your site, then publish."}, {"title": "Refunds", "url": "https://docs.example.com/billing/refunds", "section": "Billing", "text": "Refunds are issued within 14 days of purchase from Settings › Billing."}, {"title": "Install on iPhone", "url": "https://docs.example.com/setup/iphone", "section": "Setup", "text": "Install the app from the App Store and sign in with your work email."}, ] PUBLIC_READ = {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False} NO_AUTH = {"securitySchemes": [{"type": "noauth"}]} mcp = FastMCP("example-docs") @mcp.tool(annotations=PUBLIC_READ, meta=NO_AUTH) def search_docs( query: Annotated[str, Field(min_length=1, max_length=200)], limit: Annotated[int, Field(ge=1, le=10)] = 5, ) -> dict: """Search the public documentation. Returns the best-matching pages with their URLs; cite the URL you use.""" terms = [t for t in query.lower().split() if t] scored = [(sum(t in f"{p['title']} {p['text']}".lower() for t in terms), p) for p in PAGES] hits = sorted((s for s in scored if s[0] > 0), key=lambda s: -s[0])[:limit] return {"results": [{"title": p["title"], "url": p["url"], "section": p["section"], "snippet": p["text"][:300]} for _, p in hits]} @mcp.tool(annotations=PUBLIC_READ, meta=NO_AUTH) def read_docs_page(url: str) -> dict: """Return the full text of one documentation page by its URL (from search_docs or list_docs_sections).""" page = next((p for p in PAGES if p["url"] == url), None) if page is None: raise ToolError(f"No page at {url}. Use search_docs to find one.") return {"title": page["title"], "url": page["url"], "section": page["section"], "content": page["text"]} @mcp.tool(annotations=PUBLIC_READ, meta=NO_AUTH) def list_docs_sections() -> dict: """List the documentation's sections and the pages in each, with URLs.""" names = list(dict.fromkeys(p["section"] for p in PAGES)) return {"sections": [{"name": n, "pages": [{"title": p["title"], "url": p["url"]} for p in PAGES if p["section"] == n]} for n in names]} if __name__ == "__main__": # Serves Streamable HTTP at http://0.0.0.0:8000/mcp; put it behind HTTPS. mcp.run(transport="http", host="0.0.0.0", port=8000, stateless_http=True) ``` This Cloudflare Worker searches your docs JSON index and serves discovery: ```javascript // A read-only MCP server with no SDK and no database: it searches the JSON // index your site build already writes ([{ title, url, section, text }]). // Deploy as a Cloudflare Worker on your docs domain, routes /mcp and /.well-known/mcp.json. const INDEX_URL = "https://docs.example.com/search-index.json"; const VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"]; const PUBLIC_READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }; const NO_AUTH = { securitySchemes: [{ type: "noauth" }] }; const TOOLS = [ { name: "search_docs", description: "Search the public documentation. Returns the best-matching pages with their URLs; cite the URL you use.", inputSchema: { type: "object", properties: { query: { type: "string", minLength: 1, maxLength: 200 }, limit: { type: "integer", minimum: 1, maximum: 10 } }, required: ["query"] }, annotations: PUBLIC_READ, _meta: NO_AUTH }, { name: "read_docs_page", description: "Return the full text of one documentation page by its URL.", inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }, annotations: PUBLIC_READ, _meta: NO_AUTH }, { name: "list_docs_sections", description: "List the documentation's sections and the pages in each, with URLs.", inputSchema: { type: "object", properties: {} }, annotations: PUBLIC_READ, _meta: NO_AUTH }, ]; let cache = { at: 0, pages: [] }; async function pages() { if (Date.now() - cache.at > 300_000) cache = { at: Date.now(), pages: await (await fetch(INDEX_URL)).json() }; return cache.pages; } const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data) }], structuredContent: data }); async function callTool(name, args) { const all = await pages(); if (name === "search_docs") { const terms = String(args.query ?? "").toLowerCase().split(/\W+/).filter(Boolean); const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10); const results = all .map((p) => ({ p, score: terms.filter((t) => `${p.title} ${p.text}`.toLowerCase().includes(t)).length })) .filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit) .map(({ p }) => ({ title: p.title, url: p.url, section: p.section, snippet: p.text.slice(0, 300) })); return ok({ results }); } if (name === "read_docs_page") { const p = all.find((page) => page.url === args.url); return p ? ok({ title: p.title, url: p.url, section: p.section, content: p.text }) : { isError: true, content: [{ type: "text", text: `No page at ${args.url}. Use search_docs to find one.` }] }; } if (name === "list_docs_sections") { const names = [...new Set(all.map((p) => p.section))]; return ok({ sections: names.map((n) => ({ name: n, pages: all.filter((p) => p.section === n).map(({ title, url }) => ({ title, url })) })) }); } return null; } const rpc = (id, body) => Response.json({ jsonrpc: "2.0", id, ...body }); export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/.well-known/mcp.json") { return Response.json({ name: "Example Docs", endpoint: `${url.origin}/mcp`, authentication: { required: false } }); } if (url.pathname !== "/mcp") return new Response("Not found", { status: 404 }); if (request.method !== "POST") return new Response(null, { status: 405, headers: { allow: "POST" } }); let msg; try { msg = await request.json(); } catch { return rpc(null, { error: { code: -32700, message: "Parse error" } }); } if (msg.id === undefined || msg.id === null) return new Response(null, { status: 202 }); // a notification switch (msg.method) { case "initialize": { const asked = msg.params?.protocolVersion; return rpc(msg.id, { result: { protocolVersion: VERSIONS.includes(asked) ? asked : VERSIONS[0], capabilities: { tools: {} }, serverInfo: { name: "example-docs", version: "1.0.0" } } }); } case "ping": return rpc(msg.id, { result: {} }); case "tools/list": return rpc(msg.id, { result: { tools: TOOLS } }); case "tools/call": { const result = await callTool(msg.params?.name, msg.params?.arguments ?? {}); return result ? rpc(msg.id, { result }) : rpc(msg.id, { error: { code: -32602, message: "Unknown tool" } }); } default: return rpc(msg.id, { error: { code: -32601, message: "Method not found" } }); } }, }; ``` Put the endpoint on HTTPS. It must answer the POST itself: Busymate AI does not follow redirects, so `/mcp` must not redirect to `/mcp/`. ## 3. Publish a discovery file Busymate AI looks for your server on the origin of the site you add, strongest signal first: 1. `/.well-known/mcp.json`, with `endpoint` or `url` (a `servers` list also works). 2. A server card at `/.well-known/mcp/server-card.json` or `/.well-known/mcp`, with `transport.endpoint` or `remotes[].url`. 3. `/.well-known/agents.json` or `/agents.json`, with an `interfaces` entry of type `mcp`. 4. The home page: a `Link` header or a `` tag with `rel="mcp"`. Use an absolute HTTPS endpoint and serve discovery as JSON. ```json { "name": "Example Docs", "description": "Search and read the Example documentation.", "endpoint": "https://docs.example.com/mcp", "documentation": "https://docs.example.com/", "authentication": { "required": false } } ``` Or, with no file at all: ```html ``` ## 4. Connect and publish your tools An MCP-only setup needs no Website crawl source. 1. Open [Console → MCP servers](https://busymate.ai/console/mcp-servers) in the correct workspace. Enter the HTTPS endpoint, choose HTTP transport and no server credential for the public knowledge endpoint, then **Probe**. 2. Check the listed tools. Give only public knowledge reads public access. Keep account tools separate. 3. Save the connection. In [Knowledge, tools, and behavior](https://busymate.ai/console/resources), select it, save the draft, run preflight and publish. A saved connector alone is not live. 4. Ask representative questions through your assistant. Check that its calls reach your server and its citations match your tool results. Management tools: `probe_mcp_connector`, `upsert_tenant_connector`, `publish_tenant_runtime`. Preserve other settings and verify runtime publication. Keep existing crawled knowledge until tool coverage is verified. Discovery alone does not prove crawling stopped. Check serving status before changing sync settings; never delete knowledge to pass a check. ### Preview before connecting `/try/` reads discovery and the anonymous catalog without executing tools. Declared public reads and read-only candidates are offered; explicit OAuth and writes are excluded. Candidate access is established by a real requested read. An authentication refusal never triggers credential fallback. Knowledge tools replace deeper collection; business-only tools do not establish knowledge coverage. Existing assistants keep their published assignments. Previews do not grant workspace access. Supported Apps use the existing sandbox: declare `_meta.ui.resourceUri` with a `ui://` URI. Callbacks require current anonymous read-only eligibility, `"app"` visibility, and the same authenticated preview source. For an owned saved preview, reloading can revalidate expired widget access against the current site without rerunning the original tool. If current eligibility cannot be verified, the App remains unavailable. ## 5. Keep account tools behind sign-in Keep account tools behind OAuth. An anonymous 401 never grants access. For signed-in customers, use [Connect your MCP server as assistant tools](https://busymate.ai/fr/docs/guides/connect-mcp-server), and give those tools the **identified** or **delegated** access level. ## 6. Secure the public set - **Public data only.** Index only pages that are already published. - **Rate limits.** Limit requests per client IP, cap `limit` and the response size, and answer `429` with `Retry-After` when a client goes over. - **No writes.** Keep write tools off the public server, or put them on a separate endpoint behind OAuth. Never mark a write as read-only. - **Transport and CORS.** Busymate AI calls your server from its own servers over HTTPS and needs no CORS. Add CORS only for browser clients, and validate the `Origin` header as the MCP spec describes. On WordPress, advertise knowledge through a server card or `Link` header; the plugin owns `mcp.json`. For Shopify, see [the guide](https://busymate.ai/fr/docs/guides/shopify). ## Verify 1. Check the server with the MCP Inspector: ```bash # 1. What tools does the server list, with which annotations? npx @modelcontextprotocol/inspector@2.12.0 --cli https://docs.example.com/mcp --transport http --method tools/list # 2. Does a search answer WITHOUT any credentials, with page URLs? npx @modelcontextprotocol/inspector@2.12.0 --cli https://docs.example.com/mcp --transport http \ --method tools/call --tool-name search_docs --tool-arg query=refunds # 3. Is the discovery file served as JSON? curl -sS -H "accept: application/json" https://docs.example.com/.well-known/mcp.json ``` 2. The tool list shows your tools with `readOnlyHint: true`, and the search answers with no credentials, with page URLs. 3. The published workspace includes your connection, and MCP servers lists its knowledge tools with public access. 4. Ask your mate a question only your docs answer. The reply cites a URL your tool returned. | Symptom | Cause | |---|---| | Tools are saved but unavailable | The connection is not selected in the published workspace, or projection is not ready | | Connected, but nothing is public | Tools lack read-only metadata, require authentication, or are unavailable | | One tool is missing | It declares authentication or a write operation | | Answers have no links | Results do not include absolute page URLs | ### Do I need a Website crawl source? No. Connect and publish your knowledge MCP directly. For an existing crawled source, verify equivalent answers and citations before changing its sync settings. Discovery alone does not prove a working replacement. ### Which MCP protocol version is used? Streamable HTTP, protocol `2025-06-18`. JSON and event-stream responses both work, and so do stateless servers and servers with sessions. ### Can visitors reach my account tools this way? No. Preview calls are anonymous and read-only; candidates may refuse access. Account tools require customer sign-in through step 5. ### Does publishing knowledge tools grant account access? No. Keep account tools behind their own authentication and consent. Never mark customer data as public knowledge. ## Next - **[Teach your assistant your own content](https://busymate.ai/fr/docs/guides/knowledge)**: crawled sites and published text. - **[Connect your MCP server as assistant tools](https://busymate.ai/fr/docs/guides/connect-mcp-server)**: access levels for account tools. - **[Connecting your systems (MCP)](https://busymate.ai/fr/docs/connectors)**: the explanation behind both.