İçeriğe atla
Busymate AI

Bu rehber şu anda yalnızca İngilizce olarak sunulmaktadır.

Rehberler

Sitenizin bilgisini MCP ile erişilebilir kılın

Belgelerinizde arama yapan salt okunur MCP araçları yayımlayın; Busymate AI sitenizi taramak yerine bu araçlardan, sayfalarınıza bağlantılarla yanıt verir.

Bu sayfada

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.

ToolInputReturns
search_docsquery, optional limit (1–10)results: title, url, section, snippet
read_docs_pageurl from a search resulttitle, url, section, full content
list_docs_sectionsnothingsections, each with its pages and URLs
get_setup_steps (optional)optional platformordered steps, each with a url
get_pricing (optional)nothingyour published plans and the pricing page url
get_faq (optional)optional topicquestion, 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<string, unknown>) => ({ 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 <link> 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
<!-- in the <head> of your home page -->
<link rel="mcp" href="https://docs.example.com/mcp">

<!-- or as a response header on the home page:
Link: <https://docs.example.com/mcp>; rel="mcp" -->

4. Connect and publish your tools

An MCP-only setup needs no Website crawl source.

  1. Open 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, 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/<your-host> 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, 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.

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
  1. The tool list shows your tools with readOnlyHint: true, and the search answers with no credentials, with page URLs.
  2. The published workspace includes your connection, and MCP servers lists its knowledge tools with public access.
  3. Ask your mate a question only your docs answer. The reply cites a URL your tool returned.
SymptomCause
Tools are saved but unavailableThe connection is not selected in the published workspace, or projection is not ready
Connected, but nothing is publicTools lack read-only metadata, require authentication, or are unavailable
One tool is missingIt declares authentication or a write operation
Answers have no linksResults do not include absolute page URLs

Next

Sorular

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.