@hoyongjin/gitbook-mcp 1.0.0 → 2.0.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/CHANGELOG.md +44 -0
- package/README.md +52 -28
- package/dist/config.d.ts +9 -0
- package/dist/config.js +37 -1
- package/dist/gitbook/client.d.ts +15 -0
- package/dist/gitbook/client.js +29 -0
- package/dist/gitbook/import-url.d.ts +16 -9
- package/dist/gitbook/import-url.js +22 -13
- package/dist/tools/bespoke.d.ts +12 -0
- package/dist/tools/bespoke.js +166 -0
- package/dist/tools/factory.d.ts +17 -0
- package/dist/tools/factory.js +63 -0
- package/dist/tools/index.d.ts +22 -3
- package/dist/tools/index.js +49 -10
- package/dist/tools/manifest.d.ts +35 -0
- package/dist/tools/manifest.js +361 -0
- package/dist/tools/toolsets.d.ts +17 -0
- package/dist/tools/toolsets.js +28 -0
- package/package.json +5 -3
- package/dist/tools/read.d.ts +0 -4
- package/dist/tools/read.js +0 -91
- package/dist/tools/write.d.ts +0 -8
- package/dist/tools/write.js +0 -88
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ManifestRow } from "./manifest.js";
|
|
3
|
+
import { type ToolContext } from "./shared.js";
|
|
4
|
+
/**
|
|
5
|
+
* Manifest-driven registration for the generic (non-bespoke) tools. ONE factory
|
|
6
|
+
* covers every operation in every toolset — there are no per-op hand-written
|
|
7
|
+
* handlers, which is also what keeps v8 `functions` coverage healthy (one
|
|
8
|
+
* covered function, not hundreds of closures).
|
|
9
|
+
*
|
|
10
|
+
* Argument mapping needs no TS-signature parsing: the manifest gives `pathParams`
|
|
11
|
+
* (the leading positional args, in URL order) and `dataArg` (whether the method
|
|
12
|
+
* takes a body/query slot after them). The tool's input is therefore one string
|
|
13
|
+
* field per path param plus an optional freeform `body` object forwarded as the
|
|
14
|
+
* body (writes) or query (reads). Per-op typed body schemas are a deliberate
|
|
15
|
+
* non-goal here; high-value ops can be promoted to bespoke handlers later.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerGenericTool(server: McpServer, ctx: ToolContext, row: ManifestRow): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult, guard, READ_ANNOTATIONS, WRITE_ANNOTATIONS, DESTRUCTIVE_ANNOTATIONS, } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Manifest-driven registration for the generic (non-bespoke) tools. ONE factory
|
|
5
|
+
* covers every operation in every toolset — there are no per-op hand-written
|
|
6
|
+
* handlers, which is also what keeps v8 `functions` coverage healthy (one
|
|
7
|
+
* covered function, not hundreds of closures).
|
|
8
|
+
*
|
|
9
|
+
* Argument mapping needs no TS-signature parsing: the manifest gives `pathParams`
|
|
10
|
+
* (the leading positional args, in URL order) and `dataArg` (whether the method
|
|
11
|
+
* takes a body/query slot after them). The tool's input is therefore one string
|
|
12
|
+
* field per path param plus an optional freeform `body` object forwarded as the
|
|
13
|
+
* body (writes) or query (reads). Per-op typed body schemas are a deliberate
|
|
14
|
+
* non-goal here; high-value ops can be promoted to bespoke handlers later.
|
|
15
|
+
*/
|
|
16
|
+
export function registerGenericTool(server, ctx, row) {
|
|
17
|
+
server.registerTool(row.name, {
|
|
18
|
+
title: row.summary,
|
|
19
|
+
description: describe(row),
|
|
20
|
+
inputSchema: buildInputSchema(row),
|
|
21
|
+
annotations: annotationsFor(row.kind),
|
|
22
|
+
}, guard(ctx, row.name, async (input) => {
|
|
23
|
+
const args = row.pathParams.map((p) => input[p]);
|
|
24
|
+
if (row.dataArg && input.body !== undefined)
|
|
25
|
+
args.push(input.body);
|
|
26
|
+
const data = row.returnShape === "items"
|
|
27
|
+
? await ctx.gitbook.callList(row.namespace, row.op, args)
|
|
28
|
+
: await ctx.gitbook.call(row.namespace, row.op, args);
|
|
29
|
+
return jsonResult(data);
|
|
30
|
+
}, row.kind === "read" ? undefined : { audit: true }));
|
|
31
|
+
}
|
|
32
|
+
function annotationsFor(kind) {
|
|
33
|
+
if (kind === "read")
|
|
34
|
+
return READ_ANNOTATIONS;
|
|
35
|
+
if (kind === "destructive")
|
|
36
|
+
return DESTRUCTIVE_ANNOTATIONS;
|
|
37
|
+
return WRITE_ANNOTATIONS;
|
|
38
|
+
}
|
|
39
|
+
function buildInputSchema(row) {
|
|
40
|
+
const shape = {};
|
|
41
|
+
for (const p of row.pathParams) {
|
|
42
|
+
shape[p] = z.string().describe(`Path parameter: ${p}.`);
|
|
43
|
+
}
|
|
44
|
+
if (row.dataArg) {
|
|
45
|
+
shape.body = z
|
|
46
|
+
.record(z.string(), z.unknown())
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(row.kind === "read"
|
|
49
|
+
? "Optional query parameters as a JSON object (e.g. { limit, page })."
|
|
50
|
+
: "Request body as a JSON object, per the GitBook API for this operation.");
|
|
51
|
+
}
|
|
52
|
+
return shape;
|
|
53
|
+
}
|
|
54
|
+
function describe(row) {
|
|
55
|
+
let d = `${row.summary}. (${row.verb} ${row.path})`;
|
|
56
|
+
if (row.kind === "destructive")
|
|
57
|
+
d += " DESTRUCTIVE / irreversible — changes or removes data.";
|
|
58
|
+
if (row.async) {
|
|
59
|
+
d +=
|
|
60
|
+
" Asynchronous — GitBook exposes no status endpoint, so treat the result as 'started, not confirmed' and verify in GitBook.";
|
|
61
|
+
}
|
|
62
|
+
return d;
|
|
63
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { ToolContext } from "./shared.js";
|
|
3
|
+
import type { ToolsetKey } from "./toolsets.js";
|
|
4
|
+
import { type ManifestRow } from "./manifest.js";
|
|
3
5
|
export type { ToolContext } from "./shared.js";
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Whether a manifest row should register under the current config. Three gates,
|
|
8
|
+
* composed (all must hold):
|
|
9
|
+
* 1. toolset — the row's toolset is enabled.
|
|
10
|
+
* 2. readOnly — read tools always; write/destructive only when !readOnly.
|
|
11
|
+
* 3. destructive — NEW destructive ops require the GITBOOK_ALLOW_DESTRUCTIVE
|
|
12
|
+
* opt-in; `legacy` ops (the original merge tool) are exempt
|
|
13
|
+
* so back-compat is preserved.
|
|
14
|
+
* Excluded carve-outs (streaming / trivial) never register.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldRegister(row: ManifestRow, cfg: {
|
|
17
|
+
enabledToolsets: ReadonlySet<ToolsetKey>;
|
|
18
|
+
readOnly: boolean;
|
|
19
|
+
allowDestructive: boolean;
|
|
20
|
+
}): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Register all tools the current config selects, driven by the generated
|
|
23
|
+
* manifest. Bespoke rows (the 11 original tools) use their hand-written
|
|
24
|
+
* registrar; every other row is generated by the factory. `gitbook_search`
|
|
25
|
+
* maps two API ops to one tool, so registration is de-duplicated by name.
|
|
26
|
+
* Returns the registered tool names.
|
|
8
27
|
*/
|
|
9
28
|
export declare function registerTools(server: McpServer, ctx: ToolContext): string[];
|
package/dist/tools/index.js
CHANGED
|
@@ -1,17 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { MANIFEST } from "./manifest.js";
|
|
2
|
+
import { BESPOKE_TOOLS } from "./bespoke.js";
|
|
3
|
+
import { registerGenericTool } from "./factory.js";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Whether a manifest row should register under the current config. Three gates,
|
|
6
|
+
* composed (all must hold):
|
|
7
|
+
* 1. toolset — the row's toolset is enabled.
|
|
8
|
+
* 2. readOnly — read tools always; write/destructive only when !readOnly.
|
|
9
|
+
* 3. destructive — NEW destructive ops require the GITBOOK_ALLOW_DESTRUCTIVE
|
|
10
|
+
* opt-in; `legacy` ops (the original merge tool) are exempt
|
|
11
|
+
* so back-compat is preserved.
|
|
12
|
+
* Excluded carve-outs (streaming / trivial) never register.
|
|
13
|
+
*/
|
|
14
|
+
export function shouldRegister(row, cfg) {
|
|
15
|
+
if (row.exclude)
|
|
16
|
+
return false;
|
|
17
|
+
if (!cfg.enabledToolsets.has(row.toolset))
|
|
18
|
+
return false;
|
|
19
|
+
if (row.kind !== "read" && cfg.readOnly)
|
|
20
|
+
return false;
|
|
21
|
+
if (row.kind === "destructive" && !row.legacy && !cfg.allowDestructive)
|
|
22
|
+
return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register all tools the current config selects, driven by the generated
|
|
27
|
+
* manifest. Bespoke rows (the 11 original tools) use their hand-written
|
|
28
|
+
* registrar; every other row is generated by the factory. `gitbook_search`
|
|
29
|
+
* maps two API ops to one tool, so registration is de-duplicated by name.
|
|
30
|
+
* Returns the registered tool names.
|
|
7
31
|
*/
|
|
8
32
|
export function registerTools(server, ctx) {
|
|
9
|
-
const names =
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
const names = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const row of MANIFEST) {
|
|
36
|
+
if (!shouldRegister(row, ctx.config))
|
|
37
|
+
continue;
|
|
38
|
+
if (seen.has(row.name))
|
|
39
|
+
continue;
|
|
40
|
+
if (row.bespoke) {
|
|
41
|
+
const register = BESPOKE_TOOLS[row.name];
|
|
42
|
+
if (!register)
|
|
43
|
+
throw new Error(`Manifest marks ${row.name} bespoke but no registrar exists`);
|
|
44
|
+
register(server, ctx);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
registerGenericTool(server, ctx, row);
|
|
48
|
+
}
|
|
49
|
+
seen.add(row.name);
|
|
50
|
+
names.push(row.name);
|
|
12
51
|
}
|
|
13
|
-
|
|
14
|
-
ctx.logger.info("read-only mode: write tools not registered");
|
|
52
|
+
if (ctx.config.readOnly) {
|
|
53
|
+
ctx.logger.info("read-only mode: write/destructive tools not registered");
|
|
15
54
|
}
|
|
16
55
|
return names;
|
|
17
56
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type ToolKind = "read" | "write" | "destructive";
|
|
2
|
+
export type ReturnShape = "items" | "pages" | "scalar" | "void" | "stream";
|
|
3
|
+
export interface ManifestRow {
|
|
4
|
+
/** MCP tool name (grandfathered for the 11 legacy tools, else gitbook_<op>). */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** @gitbook/api method name. */
|
|
7
|
+
readonly op: string;
|
|
8
|
+
/** @gitbook/api client namespace the method lives on. */
|
|
9
|
+
readonly namespace: ApiNamespace;
|
|
10
|
+
readonly verb: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** One-line @summary from the API spec; used as the tool description base. */
|
|
13
|
+
readonly summary: string;
|
|
14
|
+
/** Path-parameter names in URL order — the leading positional args to the op. */
|
|
15
|
+
readonly pathParams: readonly string[];
|
|
16
|
+
/** Name of the body/query positional arg, or null if the op takes none (e.g. most DELETEs). */
|
|
17
|
+
readonly dataArg: string | null;
|
|
18
|
+
/** Primary @tags group. */
|
|
19
|
+
readonly tag: string;
|
|
20
|
+
readonly toolset: string;
|
|
21
|
+
readonly kind: ToolKind;
|
|
22
|
+
readonly returnShape: ReturnShape;
|
|
23
|
+
/** Async op with no status-poll endpoint (import/export): "started, not confirmed". */
|
|
24
|
+
readonly async?: boolean;
|
|
25
|
+
/** True for the 11 existing tools (hand-written handlers, not the factory). */
|
|
26
|
+
readonly bespoke?: boolean;
|
|
27
|
+
/** Exempt from the GITBOOK_ALLOW_DESTRUCTIVE opt-in (back-compat). */
|
|
28
|
+
readonly legacy?: boolean;
|
|
29
|
+
/** Carve-out: never registered (streaming / trivial / auth-only ops). */
|
|
30
|
+
readonly exclude?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Every @gitbook/api client namespace (generated from the Api class). */
|
|
33
|
+
export declare const VALID_NAMESPACES: readonly ["ads", "collections", "customHostnames", "emailDomains", "git", "integrations", "org", "orgs", "spaces", "subdomains", "urls", "user", "users"];
|
|
34
|
+
export type ApiNamespace = (typeof VALID_NAMESPACES)[number];
|
|
35
|
+
export declare const MANIFEST: readonly ManifestRow[];
|