@hoyongjin/gitbook-mcp 1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/dist/config.d.ts +58 -0
  5. package/dist/config.js +115 -0
  6. package/dist/gitbook/client.d.ts +56 -0
  7. package/dist/gitbook/client.js +109 -0
  8. package/dist/gitbook/errors.d.ts +18 -0
  9. package/dist/gitbook/errors.js +79 -0
  10. package/dist/gitbook/import-url.d.ts +23 -0
  11. package/dist/gitbook/import-url.js +51 -0
  12. package/dist/gitbook/resilient-fetch.d.ts +42 -0
  13. package/dist/gitbook/resilient-fetch.js +155 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +61 -0
  16. package/dist/limiter.d.ts +12 -0
  17. package/dist/limiter.js +44 -0
  18. package/dist/logger.d.ts +20 -0
  19. package/dist/logger.js +92 -0
  20. package/dist/metrics.d.ts +25 -0
  21. package/dist/metrics.js +71 -0
  22. package/dist/request-context.d.ts +18 -0
  23. package/dist/request-context.js +10 -0
  24. package/dist/resources.d.ts +9 -0
  25. package/dist/resources.js +56 -0
  26. package/dist/server.d.ts +14 -0
  27. package/dist/server.js +31 -0
  28. package/dist/tools/index.d.ts +9 -0
  29. package/dist/tools/index.js +17 -0
  30. package/dist/tools/read.d.ts +4 -0
  31. package/dist/tools/read.js +91 -0
  32. package/dist/tools/shared.d.ts +48 -0
  33. package/dist/tools/shared.js +99 -0
  34. package/dist/tools/write.d.ts +8 -0
  35. package/dist/tools/write.js +88 -0
  36. package/dist/transports/http.d.ts +20 -0
  37. package/dist/transports/http.js +336 -0
  38. package/dist/transports/stdio.d.ts +7 -0
  39. package/dist/transports/stdio.js +17 -0
  40. package/dist/version.d.ts +2 -0
  41. package/dist/version.js +9 -0
  42. package/package.json +72 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] - 2026-06-23
8
+
9
+ First public release: an MCP server for GitBook exposing **7 read tools** and a
10
+ **4-tool change-request write workflow** plus a page resource, over **stdio** or
11
+ **Streamable HTTP**. Built on `@gitbook/api` 0.183 and `@modelcontextprotocol/sdk`
12
+ 1.29; 103 unit/protocol tests, and verified end-to-end against the built artifact.
13
+
14
+ ### Added
15
+
16
+ - **Read tools:** `gitbook_whoami`, `gitbook_list_orgs`, `gitbook_list_spaces`,
17
+ `gitbook_get_space`, `gitbook_list_pages`, `gitbook_get_page` (markdown or
18
+ structured document), `gitbook_search` (org- or space-scoped — exactly one).
19
+ - **Change-request write tools:** `gitbook_create_change_request`,
20
+ `gitbook_import_content` (web-URL import; http(s)-only, embedded-credential
21
+ guard), `gitbook_comment_change_request`, and `gitbook_merge_change_request`
22
+ (destructive — a `result:"conflicts"` outcome is surfaced as an error because it
23
+ does not publish). Write tools are **not registered** in read-only mode, so they
24
+ never appear in `tools/list`.
25
+ - **Resource:** GitBook pages at `gitbook://{spaceId}/{pageId}` (markdown), with the
26
+ same error-classification + token-redaction guarantees as the tools.
27
+ - **Transports:** stdio (default, local) and Streamable HTTP — per-session
28
+ server/transport; constant-time bearer auth; DNS-rebinding protection with a
29
+ configurable public-host allow-list (`GITBOOK_HTTP_ALLOWED_HOSTS` /
30
+ `GITBOOK_HTTP_ALLOWED_ORIGINS`); loopback-bound by default and fail-closed on a
31
+ non-loopback bind without an auth token; a bounded session store (cap + idle and
32
+ absolute-lifetime reapers); per-IP rate limiting; `/healthz` `/livez` `/readyz`
33
+ probes; and a bearer-gated Prometheus `/metrics`.
34
+ - **Resilience:** per-request timeouts, bounded retries with full-jitter backoff,
35
+ `Retry-After` / `X-RateLimit-Reset` honored on 429 **and** 5xx, an outbound
36
+ concurrency limiter, and **idempotency-aware retries** (5xx/timeout retries are
37
+ suppressed for non-idempotent writes to avoid duplicate change requests, comments,
38
+ or import runs).
39
+ - **Security:** token read from the environment only (never `argv`); secret
40
+ redaction across logs, error messages, and tool/resource results; read-only mode;
41
+ and a token-length floor that keeps every accepted token redactable.
42
+ - **Observability:** in-process Prometheus metrics (tool calls/errors, fetch
43
+ retries, rate-limit hits, active sessions, resource reads); request-correlation
44
+ ids stamped on every log line via AsyncLocalStorage; info-level audit logs for
45
+ write/destructive tools. Logs are JSON to **stderr only** (stdout is the protocol).
46
+ - **Packaging / CI:** npm provenance (`--provenance` + `id-token`) with a resolvable
47
+ `repository`; a `files` allowlist with no shipped source maps; a release-tag ==
48
+ `package.json` version guard before publish; Node 20/22 CI
49
+ (typecheck → lint → format → build → coverage gate → `npm audit`); Dependabot;
50
+ CodeQL; and a Dockerfile for the HTTP transport.
51
+
52
+ > Published as `@hoyongjin/gitbook-mcp` from `github.com/HoYongJin/gitbook-mcp`.
53
+ > The CI publish job runs on a GitHub Release and requires the `@hoyongjin` scope to
54
+ > be owned by the publishing npm account and an `NPM_TOKEN` secret in the repo.
55
+
56
+ [1.0.0]: https://github.com/HoYongJin/gitbook-mcp/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gitbook-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # gitbook-mcp
2
+
3
+ A **[Model Context Protocol](https://modelcontextprotocol.io) server for GitBook.**
4
+ It lets an MCP host (Claude Code, Claude Desktop, Cursor, …) **read** your GitBook
5
+ content and drive a **change-request write workflow**, over **stdio** or
6
+ **streamable HTTP**.
7
+
8
+ Built on the official [`@gitbook/api`](https://www.npmjs.com/package/@gitbook/api)
9
+ client and [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) 1.x.
10
+
11
+ - **11 tools** — 7 read + 4 change-request write (write tools are hidden in read-only mode).
12
+ - **Resources** — pages addressable as `gitbook://{spaceId}/{pageId}`.
13
+ - **Two transports** — stdio (local) and streamable HTTP (multi-session, bearer-gated).
14
+ - **Resilient** — request timeouts, bounded retries with full-jitter backoff, rate-limit aware.
15
+ - **Safe** — read-only mode, env-only token, secret redaction, localhost-bound HTTP.
16
+
17
+ > **Status:** the read path and the change-request write workflow are complete and
18
+ > tested (103 unit/protocol tests). The **stdio** transport is ready for local
19
+ > IDE/CLI use; the **HTTP** transport is hardened for hosted use — bearer auth,
20
+ > DNS-rebinding protection, a session cap + idle reaper, per-IP rate limiting,
21
+ > health/readiness probes, and Prometheus metrics. To publish: push to
22
+ > `github.com/HoYongJin/gitbook-mcp`, ensure your npm account owns the `@hoyongjin`
23
+ > scope, then create a GitHub release (CI publishes to npm with provenance).
24
+
25
+ ---
26
+
27
+ ## ⚠️ What GitBook's API can and cannot write
28
+
29
+ GitBook's public API has **no direct "edit this page body" endpoint**
30
+ (`createPage`/`updatePage` exist only as the GitBook Assistant's _internal_ tool
31
+ names, not as REST methods). Content authoring goes through:
32
+
33
+ 1. **Change requests** — open a draft (`gitbook_create_change_request`), review, then `gitbook_merge_change_request` to publish.
34
+ 2. **Content import** — `gitbook_import_content` imports a **web page URL** into a space/change-request/page, AI-enhanced by default. This is the supported "write content" primitive. Imports are asynchronous, and GitBook exposes no status-poll endpoint.
35
+ 3. **Git Sync** — for fine-grained, paragraph-level authoring, connect the space to a Git repo and edit markdown there. _(Out of scope for this server; it is the right path if you need precise prose edits.)_
36
+
37
+ So this server is **read-rich** with a **coarse, review-gated write** path. It
38
+ cannot rewrite a specific paragraph via the API.
39
+
40
+ ---
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ # From source (local use):
46
+ cd gitbook-mcp && npm install && npm run build
47
+ # → run with: node dist/index.js (or `npm start`)
48
+
49
+ # Or, once published:
50
+ npx @hoyongjin/gitbook-mcp
51
+ ```
52
+
53
+ > The package is **scoped** to `@hoyongjin/gitbook-mcp` (the unscoped `gitbook-mcp`
54
+ > on npm belongs to a different project). Make sure your npm account owns the
55
+ > `@hoyongjin` scope before publishing.
56
+
57
+ Create a token at **GitBook → Settings → Developer → Personal access tokens**.
58
+
59
+ ## Configure in your MCP client
60
+
61
+ ### Claude Code
62
+
63
+ ```bash
64
+ # Local build:
65
+ claude mcp add gitbook \
66
+ --env GITBOOK_TOKEN=gb_api_xxx \
67
+ -- node /absolute/path/to/gitbook-mcp/dist/index.js
68
+ ```
69
+
70
+ ### `.mcp.json` / Claude Desktop config
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "gitbook": {
76
+ "command": "node",
77
+ "args": ["/absolute/path/to/gitbook-mcp/dist/index.js"],
78
+ "env": { "GITBOOK_TOKEN": "gb_api_xxx" }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ (After publishing, replace `command`/`args` with
85
+ `"command": "npx", "args": ["@hoyongjin/gitbook-mcp"]`.)
86
+
87
+ Run read-only (recommended unless you need writes) by adding `"--read-only"` to
88
+ `args`, or `"GITBOOK_READONLY": "true"` to `env`.
89
+
90
+ ## Configuration
91
+
92
+ All configuration is via environment variables (see [`.env.example`](./.env.example)).
93
+ The token is **never** taken from a CLI argument.
94
+
95
+ | Variable | Default | Description |
96
+ | -------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
97
+ | `GITBOOK_TOKEN` | — | **Required.** Personal access token (account-wide privileges). |
98
+ | `GITBOOK_ENDPOINT` | `https://api.gitbook.com` | API base URL. |
99
+ | `GITBOOK_READONLY` | `false` | Hide all write tools when true. |
100
+ | `GITBOOK_TRANSPORT` | `stdio` | `stdio` or `http`. |
101
+ | `GITBOOK_HTTP_HOST` | `127.0.0.1` | HTTP bind host. |
102
+ | `GITBOOK_HTTP_PORT` | `3000` | HTTP port. |
103
+ | `GITBOOK_HTTP_AUTH_TOKEN` | — | Bearer token required on the HTTP transport. |
104
+ | `GITBOOK_HTTP_MAX_SESSIONS` | `256` | Concurrent-session cap; new initializes past it get 503 (1–100000). |
105
+ | `GITBOOK_HTTP_SESSION_TTL_MS` | `300000` | Idle-session reaper TTL (1 000–86 400 000). |
106
+ | `GITBOOK_HTTP_SESSION_MAX_LIFETIME_MS` | `3600000` | Absolute session lifetime; reaped regardless of activity. |
107
+ | `GITBOOK_HTTP_TRUST_PROXY` | `false` | Trust `X-Forwarded-*` (enable only behind a trusted reverse proxy). |
108
+ | `GITBOOK_HTTP_ALLOWED_HOSTS` | — | Comma-separated hosts appended to the DNS-rebinding allow-list. **Required for proxied/non-loopback access** (e.g. `mcp.example.com`). |
109
+ | `GITBOOK_HTTP_ALLOWED_ORIGINS` | — | Comma-separated origins appended to the allow-list (e.g. `https://mcp.example.com`). |
110
+ | `GITBOOK_HTTP_RATE_LIMIT_WINDOW_MS` | `60000` | Rate-limit window per client IP. |
111
+ | `GITBOOK_HTTP_RATE_LIMIT_MAX` | `120` | Max `/mcp` requests per window per IP; `0` disables. |
112
+ | `GITBOOK_LOG_LEVEL` | `info` | `debug`/`info`/`warn`/`error` (logs → stderr). |
113
+ | `GITBOOK_TIMEOUT_MS` | `30000` | Per-request timeout (1 000–120 000). |
114
+ | `GITBOOK_MAX_RETRIES` | `5` | Max retries on 429/5xx/transport errors (0–10). |
115
+ | `GITBOOK_MAX_CONCURRENCY` | `8` | Max concurrent outbound GitBook HTTP requests per instance (1–256). |
116
+
117
+ CLI flags: `--stdio` / `--http` (transport), `--read-only`, `--port <n>`.
118
+
119
+ ## Tools
120
+
121
+ | Tool | R/W | Purpose |
122
+ | -------------------------------- | ----------------------- | ------------------------------------------------------------ |
123
+ | `gitbook_whoami` | read | Authenticated user. |
124
+ | `gitbook_list_orgs` | read | Organizations you can access. |
125
+ | `gitbook_list_spaces` | read | Spaces in an org. |
126
+ | `gitbook_get_space` | read | One space by id. |
127
+ | `gitbook_list_pages` | read | Page tree of a space (not paginated). |
128
+ | `gitbook_get_page` | read | A page as **markdown** (default) or structured **document**. |
129
+ | `gitbook_search` | read | Full-text search (org- or space-scoped; exactly one). |
130
+ | `gitbook_create_change_request` | write | Open a draft change request. |
131
+ | `gitbook_import_content` | write | Import a URL into a space/change-request (AI-enhanced). |
132
+ | `gitbook_comment_change_request` | write | Post a markdown comment on a change request. |
133
+ | `gitbook_merge_change_request` | **write · destructive** | Publish a change request into the live space. |
134
+
135
+ Tools that paginate (`list_orgs`, `list_spaces`, `search`) accept `limit` + a
136
+ `cursor`, and return `nextCursor` when more results exist — pass it back to fetch
137
+ the next page.
138
+
139
+ Typical write flow: `create_change_request` → `import_content` (target that CR) →
140
+ review in GitBook → `merge_change_request`.
141
+
142
+ ## Resources
143
+
144
+ Pages are exposed as resources at `gitbook://{spaceId}/{pageId}` (rendered as
145
+ markdown). Enumeration is intentionally not provided — discover page ids via
146
+ `gitbook_list_pages` / `gitbook_search`, then read by URI.
147
+
148
+ ## Transports
149
+
150
+ - **stdio** (default): one server per process; the token stays in-process. Best
151
+ for local IDE/CLI integrations.
152
+ - **streamable HTTP** (`--http`): one MCP session per `mcp-session-id`. Binds to
153
+ `127.0.0.1`, enables DNS-rebinding protection (Host/Origin allow-lists), and —
154
+ when `GITBOOK_HTTP_AUTH_TOKEN` is set — requires a constant-time-compared bearer
155
+ token on every request. Binding to a non-loopback host **without** an auth token
156
+ is refused (fail-closed). Sessions are capped and idle-reaped; the `/mcp`
157
+ endpoint is per-IP rate-limited. Operational endpoints: `GET /healthz` `/livez`
158
+ `/readyz` (unauthenticated probes) and `GET /metrics` (Prometheus; bearer-gated
159
+ when an auth token is set). Behind a TLS-terminating reverse proxy, set
160
+ `GITBOOK_HTTP_TRUST_PROXY=true` **and** `GITBOOK_HTTP_ALLOWED_HOSTS=<your-public-host>`
161
+ — DNS-rebinding protection matches the exact `Host` header, so a proxied or
162
+ non-loopback deployment (including the Docker image, which binds `0.0.0.0`) must
163
+ add its public host(s) or every request is rejected with 403 `Invalid Host`. A
164
+ [`Dockerfile`](./Dockerfile) is provided. See [`SECURITY.md`](./SECURITY.md).
165
+
166
+ ```bash
167
+ GITBOOK_TOKEN=… GITBOOK_HTTP_AUTH_TOKEN=… gitbook-mcp --http --port 3000
168
+ # → POST/GET/DELETE http://127.0.0.1:3000/mcp · GET /healthz · GET /metrics
169
+ ```
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ npm install
175
+ npm run typecheck # tsc on src + test
176
+ npm run lint # eslint
177
+ npm run format # prettier --write (format:check verifies)
178
+ npm test # vitest — 103 tests across 12 files
179
+ npm run test:coverage # + v8 coverage with an 80% gate
180
+ npm run build # tsc → dist/ (+ chmod the bin)
181
+ ```
182
+
183
+ CI (`.github/workflows/ci.yml`) runs `npm audit` → typecheck → lint → format:check
184
+ → build → coverage on Node 20 & 22, then asserts a clean `git diff`. CodeQL and
185
+ Dependabot run separately.
186
+
187
+ ## Architecture
188
+
189
+ ```
190
+ src/
191
+ index.ts launcher: load config → pick transport → graceful shutdown
192
+ config.ts zod-validated env + CLI overrides (frozen Config)
193
+ logger.ts structured stderr logging + secret redaction + correlation
194
+ request-context.ts AsyncLocalStorage requestId/tool (log correlation)
195
+ metrics.ts in-process Prometheus registry (served at /metrics)
196
+ limiter.ts async concurrency semaphore (outbound rate control)
197
+ server.ts createServer(): McpServer factory (tools + resources)
198
+ resources.ts gitbook://{spaceId}/{pageId}
199
+ version.ts stable SERVER_NAME + runtime SERVER_VERSION (from package.json)
200
+ gitbook/
201
+ client.ts typed wrapper over @gitbook/api (cursor pagination → {items,nextCursor})
202
+ resilient-fetch.ts timeouts + retry/backoff + concurrency cap; injected as the client's fetch
203
+ import-url.ts SSRF guard for import sourceUrl (http(s) only, no credentials)
204
+ errors.ts HTTP/transport error classification → safe messages
205
+ tools/
206
+ read.ts write.ts one registrar each; index.ts gates writes by read-only mode
207
+ shared.ts ToolContext + result/guard helpers + annotation presets
208
+ transports/
209
+ stdio.ts http.ts the two transports (http: sessions, rate limit, health, metrics)
210
+ ```
211
+
212
+ For agent-facing contributor conventions and the dependency gotchas below, see
213
+ [`CLAUDE.md`](./CLAUDE.md).
214
+
215
+ ### Notes / gotchas (pinned to @gitbook/api 0.183.0)
216
+
217
+ - **tsconfig uses `moduleResolution: "Bundler"`**, not `NodeNext`. `@gitbook/api`
218
+ 0.183 ships a `.d.ts` that re-exports with extensionless relative specifiers
219
+ (`export * from './client'`); under NodeNext those don't resolve and the entire
220
+ client surface (and the `GitBookAPI` namespace members) silently disappears.
221
+ Bundler resolution accepts them, giving full typing with no facade. Internal
222
+ imports use explicit `.js` extensions so the emitted ESM runs on Node.
223
+ - **Import runs live under the singular `client.org` namespace**, not the plural
224
+ `client.orgs` (which holds list/search). They are different runtime objects.
225
+ - **`gitbook_get_page` markdown vs document**: `format=markdown` returns rendered
226
+ markdown; `format=document` returns GitBook's structured Document JSON.
227
+ - Node ≥ 20 (global `fetch`, `AbortSignal.any`).
228
+
229
+ ## License
230
+
231
+ [MIT](./LICENSE)
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Centralized, validated configuration. Read `process.env` (and a few CLI
3
+ * flags) EXACTLY ONCE, here, into a frozen typed object. Nothing else in the
4
+ * codebase should touch `process.env`.
5
+ *
6
+ * Security: the GitBook token is accepted ONLY via the environment, never a
7
+ * CLI argument — process arguments leak through `ps`, `/proc`, and shell
8
+ * history. (GitHub's and Notion's official MCP servers do the same.)
9
+ */
10
+ export type Transport = "stdio" | "http";
11
+ export type LogLevel = "debug" | "info" | "warn" | "error";
12
+ export interface Config {
13
+ readonly token: string;
14
+ readonly endpoint: string;
15
+ readonly userAgent: string;
16
+ readonly readOnly: boolean;
17
+ readonly transport: Transport;
18
+ readonly http: {
19
+ readonly host: string;
20
+ readonly port: number;
21
+ /** Optional bearer token required on the HTTP transport (undefined = none). */
22
+ readonly authToken: string | undefined;
23
+ /** Hard cap on concurrent MCP sessions; new initializes past it are rejected (503). */
24
+ readonly maxSessions: number;
25
+ /** Idle session time-to-live in ms; sessions idle longer are reaped. */
26
+ readonly sessionIdleTtlMs: number;
27
+ /** Absolute session lifetime in ms; sessions older than this are reaped regardless of activity. */
28
+ readonly sessionMaxLifetimeMs: number;
29
+ /** Trust X-Forwarded-* (set true only behind a trusted reverse proxy). */
30
+ readonly trustProxy: boolean;
31
+ /**
32
+ * Extra entries appended to the DNS-rebinding Host allow-list, matched against
33
+ * the full incoming `Host` header (e.g. "mcp.example.com" or "mcp.example.com:8080").
34
+ * REQUIRED for proxied/non-loopback access: the derived list only covers the
35
+ * bind host + loopback, so a public hostname must be added here or every
36
+ * request is rejected with 403 "Invalid Host header".
37
+ */
38
+ readonly allowedHosts: readonly string[];
39
+ /** Extra entries appended to the DNS-rebinding Origin allow-list (e.g. "https://mcp.example.com"). */
40
+ readonly allowedOrigins: readonly string[];
41
+ readonly rateLimit: {
42
+ readonly windowMs: number;
43
+ /** Max requests per window per client IP; 0 disables rate limiting. */
44
+ readonly max: number;
45
+ };
46
+ };
47
+ readonly logLevel: LogLevel;
48
+ readonly request: {
49
+ readonly timeoutMs: number;
50
+ readonly maxRetries: number;
51
+ /** Max concurrent outbound GitBook requests per instance (bounds initiation→headers, not body transfer). */
52
+ readonly maxConcurrency: number;
53
+ };
54
+ }
55
+ export declare class ConfigError extends Error {
56
+ readonly name = "ConfigError";
57
+ }
58
+ export declare function loadConfig(env?: NodeJS.ProcessEnv, argv?: readonly string[]): Config;
package/dist/config.js ADDED
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ export class ConfigError extends Error {
3
+ name = "ConfigError";
4
+ }
5
+ /** Coerce common truthy/falsy env strings to a boolean. */
6
+ const boolish = z.union([z.boolean(), z.string()]).transform((v) => {
7
+ if (typeof v === "boolean")
8
+ return v;
9
+ return ["1", "true", "yes", "on"].includes(v.trim().toLowerCase());
10
+ });
11
+ /** Split a comma-separated env value into a trimmed, non-empty list. */
12
+ const csv = (v) => v
13
+ ? v
14
+ .split(",")
15
+ .map((s) => s.trim())
16
+ .filter((s) => s.length > 0)
17
+ : [];
18
+ const EnvSchema = z.object({
19
+ GITBOOK_TOKEN: z
20
+ .string({ message: "GITBOOK_TOKEN is required" })
21
+ .trim()
22
+ // Floor matches redactSecret's >=6 guard (logger.ts) so EVERY accepted token
23
+ // is always redactable — there is no length window where the literal token
24
+ // would pass config yet be exempt from log/error scrubbing. Real GitBook
25
+ // tokens are far longer (gb_api_… + 40+ chars), so this rejects nothing real.
26
+ .min(6, "GITBOOK_TOKEN looks too short to be a valid GitBook token"),
27
+ GITBOOK_ENDPOINT: z.string().url().default("https://api.gitbook.com"),
28
+ GITBOOK_USER_AGENT: z.string().min(1).optional(),
29
+ GITBOOK_READONLY: boolish.default(false),
30
+ GITBOOK_TRANSPORT: z.enum(["stdio", "http"]).default("stdio"),
31
+ GITBOOK_HTTP_HOST: z.string().min(1).default("127.0.0.1"),
32
+ GITBOOK_HTTP_PORT: z.coerce.number().int().min(1).max(65535).default(3000),
33
+ GITBOOK_HTTP_AUTH_TOKEN: z.string().min(1).optional(),
34
+ GITBOOK_HTTP_MAX_SESSIONS: z.coerce.number().int().min(1).max(100000).default(256),
35
+ GITBOOK_HTTP_SESSION_TTL_MS: z.coerce.number().int().min(1000).max(86400000).default(300000),
36
+ GITBOOK_HTTP_SESSION_MAX_LIFETIME_MS: z.coerce
37
+ .number()
38
+ .int()
39
+ .min(1000)
40
+ .max(86400000)
41
+ .default(3600000),
42
+ GITBOOK_HTTP_TRUST_PROXY: boolish.default(false),
43
+ GITBOOK_HTTP_ALLOWED_HOSTS: z.string().optional(),
44
+ GITBOOK_HTTP_ALLOWED_ORIGINS: z.string().optional(),
45
+ GITBOOK_HTTP_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().min(1000).max(3600000).default(60000),
46
+ GITBOOK_HTTP_RATE_LIMIT_MAX: z.coerce.number().int().min(0).max(1000000).default(120),
47
+ GITBOOK_LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
48
+ GITBOOK_TIMEOUT_MS: z.coerce.number().int().min(1000).max(120000).default(30000),
49
+ GITBOOK_MAX_RETRIES: z.coerce.number().int().min(0).max(10).default(5),
50
+ GITBOOK_MAX_CONCURRENCY: z.coerce.number().int().min(1).max(256).default(8),
51
+ });
52
+ /** CLI flags that override env (transport selection + read-only safety). */
53
+ function parseArgvOverrides(argv) {
54
+ const out = {};
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const a = argv[i];
57
+ if (a === "--http")
58
+ out.transport = "http";
59
+ else if (a === "--stdio")
60
+ out.transport = "stdio";
61
+ else if (a === "--read-only" || a === "--readonly")
62
+ out.readOnly = true;
63
+ else if (a === "--port") {
64
+ const next = argv[i + 1];
65
+ const n = next ? Number(next) : NaN;
66
+ if (Number.isInteger(n) && n >= 1 && n <= 65535) {
67
+ out.port = n;
68
+ i++;
69
+ }
70
+ else {
71
+ throw new ConfigError(`--port requires an integer 1-65535 (got "${next ?? ""}")`);
72
+ }
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+ export function loadConfig(env = process.env, argv = process.argv.slice(2)) {
78
+ const parsed = EnvSchema.safeParse(env);
79
+ if (!parsed.success) {
80
+ const issues = parsed.error.issues
81
+ .map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`)
82
+ .join("\n");
83
+ throw new ConfigError(`Invalid configuration:\n${issues}`);
84
+ }
85
+ const e = parsed.data;
86
+ const overrides = parseArgvOverrides(argv);
87
+ return Object.freeze({
88
+ token: e.GITBOOK_TOKEN,
89
+ endpoint: e.GITBOOK_ENDPOINT,
90
+ userAgent: e.GITBOOK_USER_AGENT ?? `gitbook-mcp`,
91
+ readOnly: overrides.readOnly ?? e.GITBOOK_READONLY,
92
+ transport: overrides.transport ?? e.GITBOOK_TRANSPORT,
93
+ http: Object.freeze({
94
+ host: e.GITBOOK_HTTP_HOST,
95
+ port: overrides.port ?? e.GITBOOK_HTTP_PORT,
96
+ authToken: e.GITBOOK_HTTP_AUTH_TOKEN,
97
+ maxSessions: e.GITBOOK_HTTP_MAX_SESSIONS,
98
+ sessionIdleTtlMs: e.GITBOOK_HTTP_SESSION_TTL_MS,
99
+ sessionMaxLifetimeMs: e.GITBOOK_HTTP_SESSION_MAX_LIFETIME_MS,
100
+ trustProxy: e.GITBOOK_HTTP_TRUST_PROXY,
101
+ allowedHosts: Object.freeze(csv(e.GITBOOK_HTTP_ALLOWED_HOSTS)),
102
+ allowedOrigins: Object.freeze(csv(e.GITBOOK_HTTP_ALLOWED_ORIGINS)),
103
+ rateLimit: Object.freeze({
104
+ windowMs: e.GITBOOK_HTTP_RATE_LIMIT_WINDOW_MS,
105
+ max: e.GITBOOK_HTTP_RATE_LIMIT_MAX,
106
+ }),
107
+ }),
108
+ logLevel: e.GITBOOK_LOG_LEVEL,
109
+ request: Object.freeze({
110
+ timeoutMs: e.GITBOOK_TIMEOUT_MS,
111
+ maxRetries: e.GITBOOK_MAX_RETRIES,
112
+ maxConcurrency: e.GITBOOK_MAX_CONCURRENCY,
113
+ }),
114
+ });
115
+ }
@@ -0,0 +1,56 @@
1
+ import { GitBookAPI } from "@gitbook/api";
2
+ import type { ChangeRequest, Comment, ContentImportRun, Organization, RevisionPage, SearchPageResult, SearchSpaceResult, Space, User } from "@gitbook/api";
3
+ import type { Config } from "../config.js";
4
+ import type { Logger } from "../logger.js";
5
+ /** A single page of a cursor-paginated GitBook list. */
6
+ export interface Page<T> {
7
+ readonly items: T[];
8
+ /** Opaque cursor for the next page; absent on the last page. */
9
+ readonly nextCursor: string | undefined;
10
+ }
11
+ export interface ListParams {
12
+ limit?: number;
13
+ cursor?: string;
14
+ }
15
+ export interface ImportContentParams {
16
+ orgId: string;
17
+ spaceId: string;
18
+ sourceUrl: string;
19
+ changeRequestId?: string;
20
+ pageId?: string;
21
+ enhance?: boolean;
22
+ }
23
+ export type PageFormat = "markdown" | "document";
24
+ /**
25
+ * Thin, typed, resilient wrapper over @gitbook/api. Exposes only the domain
26
+ * operations the MCP tools need, normalizes cursor pagination, and centralizes
27
+ * the namespace gotchas (import runs live on the SINGULAR `org`; list/search on
28
+ * the PLURAL `orgs`). Inject a mock in tests.
29
+ */
30
+ export declare class GitBookClient {
31
+ private readonly api;
32
+ constructor(config: Config, logger: Logger,
33
+ /** Test seam: override the underlying client (e.g. a stub). */
34
+ api?: GitBookAPI);
35
+ getAuthenticatedUser(): Promise<User>;
36
+ listOrganizations(params?: ListParams): Promise<Page<Organization>>;
37
+ listSpaces(orgId: string, params?: ListParams): Promise<Page<Space>>;
38
+ getSpace(spaceId: string): Promise<Space>;
39
+ /** Page tree of the space's current revision. NOT paginated (no cursor). */
40
+ listPages(spaceId: string): Promise<RevisionPage[]>;
41
+ getPage(spaceId: string, pageId: string, format: PageFormat): Promise<RevisionPage>;
42
+ searchOrganization(orgId: string, query: string, params?: ListParams): Promise<Page<SearchSpaceResult>>;
43
+ searchSpace(spaceId: string, query: string, params?: ListParams): Promise<Page<SearchPageResult>>;
44
+ createChangeRequest(spaceId: string, subject?: string): Promise<ChangeRequest>;
45
+ /**
46
+ * GitBook's content-write primitive. Import runs are async and live on the
47
+ * SINGULAR `org` namespace. Returns the initial ContentImportRun (status
48
+ * pending/in-progress); GitBook exposes no poll endpoint.
49
+ */
50
+ importContent(params: ImportContentParams): Promise<ContentImportRun>;
51
+ commentOnChangeRequest(spaceId: string, changeRequestId: string, markdown: string, page?: string): Promise<Comment>;
52
+ mergeChangeRequest(spaceId: string, changeRequestId: string): Promise<{
53
+ revision: string;
54
+ result: "merge" | "conflicts";
55
+ }>;
56
+ }
@@ -0,0 +1,109 @@
1
+ import { GitBookAPI } from "@gitbook/api";
2
+ import { SERVER_VERSION } from "../version.js";
3
+ import { createResilientFetch } from "./resilient-fetch.js";
4
+ import { assertSafeImportUrl } from "./import-url.js";
5
+ /**
6
+ * Thin, typed, resilient wrapper over @gitbook/api. Exposes only the domain
7
+ * operations the MCP tools need, normalizes cursor pagination, and centralizes
8
+ * the namespace gotchas (import runs live on the SINGULAR `org`; list/search on
9
+ * the PLURAL `orgs`). Inject a mock in tests.
10
+ */
11
+ export class GitBookClient {
12
+ api;
13
+ constructor(config, logger,
14
+ /** Test seam: override the underlying client (e.g. a stub). */
15
+ api) {
16
+ this.api =
17
+ api ??
18
+ new GitBookAPI({
19
+ authToken: config.token,
20
+ endpoint: config.endpoint,
21
+ userAgent: `${config.userAgent}/${SERVER_VERSION}`,
22
+ // The 0.183 client wraps `serviceBinding.fetch`; injecting our
23
+ // resilient fetch here gives retries+timeouts to every method.
24
+ serviceBinding: {
25
+ fetch: createResilientFetch({
26
+ timeoutMs: config.request.timeoutMs,
27
+ maxRetries: config.request.maxRetries,
28
+ maxConcurrency: config.request.maxConcurrency,
29
+ logger: logger.child({ component: "gitbook-fetch" }),
30
+ }),
31
+ },
32
+ });
33
+ }
34
+ // ── reads ──────────────────────────────────────────────────────────────────
35
+ async getAuthenticatedUser() {
36
+ return (await this.api.user.getAuthenticatedUser()).data;
37
+ }
38
+ async listOrganizations(params = {}) {
39
+ const res = await this.api.orgs.listOrganizationsForAuthenticatedUser({
40
+ limit: params.limit,
41
+ page: params.cursor,
42
+ });
43
+ return { items: res.data.items, nextCursor: res.data.next?.page };
44
+ }
45
+ async listSpaces(orgId, params = {}) {
46
+ const res = await this.api.orgs.listSpacesInOrganizationById(orgId, {
47
+ limit: params.limit,
48
+ page: params.cursor,
49
+ });
50
+ return { items: res.data.items, nextCursor: res.data.next?.page };
51
+ }
52
+ async getSpace(spaceId) {
53
+ return (await this.api.spaces.getSpaceById(spaceId)).data;
54
+ }
55
+ /** Page tree of the space's current revision. NOT paginated (no cursor). */
56
+ async listPages(spaceId) {
57
+ return (await this.api.spaces.listPages(spaceId)).data.pages;
58
+ }
59
+ async getPage(spaceId, pageId, format) {
60
+ return (await this.api.spaces.getPageById(spaceId, pageId, { format })).data;
61
+ }
62
+ async searchOrganization(orgId, query, params = {}) {
63
+ const res = await this.api.orgs.searchOrganizationContent(orgId, {
64
+ query,
65
+ limit: params.limit,
66
+ page: params.cursor,
67
+ });
68
+ return { items: res.data.items, nextCursor: res.data.next?.page };
69
+ }
70
+ async searchSpace(spaceId, query, params = {}) {
71
+ const res = await this.api.spaces.searchSpaceContent(spaceId, {
72
+ query,
73
+ limit: params.limit,
74
+ page: params.cursor,
75
+ });
76
+ return { items: res.data.items, nextCursor: res.data.next?.page };
77
+ }
78
+ // ── writes (change-request workflow) ────────────────────────────────────────
79
+ async createChangeRequest(spaceId, subject) {
80
+ return (await this.api.spaces.createChangeRequest(spaceId, subject ? { subject } : {})).data;
81
+ }
82
+ /**
83
+ * GitBook's content-write primitive. Import runs are async and live on the
84
+ * SINGULAR `org` namespace. Returns the initial ContentImportRun (status
85
+ * pending/in-progress); GitBook exposes no poll endpoint.
86
+ */
87
+ async importContent(params) {
88
+ return (await this.api.org.startImportRun(params.orgId, {
89
+ // Scheme/credential validation + normalize: http(s) only, reject embedded
90
+ // credentials. (Internal-target/SSRF protection is GitBook-side; it performs the fetch.)
91
+ source: { type: "website", url: assertSafeImportUrl(params.sourceUrl) },
92
+ target: {
93
+ space: params.spaceId,
94
+ changeRequest: params.changeRequestId,
95
+ page: params.pageId,
96
+ },
97
+ enhance: params.enhance ?? true,
98
+ })).data;
99
+ }
100
+ async commentOnChangeRequest(spaceId, changeRequestId, markdown, page) {
101
+ return (await this.api.spaces.postCommentInChangeRequest(spaceId, changeRequestId, {
102
+ body: { markdown },
103
+ ...(page ? { page } : {}),
104
+ })).data;
105
+ }
106
+ async mergeChangeRequest(spaceId, changeRequestId) {
107
+ return (await this.api.spaces.mergeChangeRequest(spaceId, changeRequestId)).data;
108
+ }
109
+ }