Skip to content

Commit f61f8ef

Browse files
authored
Merge pull request #19 from paradoxbound/feature/v2.5.0-monorepo
Docker: atomic CD pipeline with pre-merge validation
2 parents 5a33669 + 726c64c commit f61f8ef

4 files changed

Lines changed: 730 additions & 93 deletions

File tree

.github/copilot-instructions.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# BookStack MCP Server - AI Coding Agent Instructions
2+
3+
## Project Overview
4+
BookStack MCP v2.5.0 is a **monorepo** providing a Model Context Protocol server for BookStack. Architecture: `packages/core` (shared API client + types) and `packages/stdio` (MCP server with stdio transport). The core design uses **native `fetch` only** (no axios) and `McpServer` + `registerTool()` for clean, maintainable code.
5+
6+
## Monorepo Structure
7+
```
8+
packages/core/ # @bookstack-mcp/core - BookStack API client
9+
src/
10+
bookstack-client.ts # HTTP client, all API methods, response enhancement
11+
types.ts # Shared TypeScript types
12+
tests/ # Functional tests with vitest
13+
14+
packages/stdio/ # bookstack-mcp-stdio - MCP server
15+
src/
16+
index.ts # Entry point: env validation, tool registration, stdio transport
17+
```
18+
19+
**Key Design**: Core is a dependency of stdio. Tests import from `@bookstack-mcp/core`. Root commands (`npm run build`, `npm test`) delegate to workspaces.
20+
21+
## Critical Patterns
22+
23+
### Error Handling: Native Fetch (No Axios)
24+
All HTTP requests use native `fetch`. On error, set `error.status` and `error.response`:
25+
26+
```typescript
27+
// packages/core/src/bookstack-client.ts - request() method
28+
if (!res.ok) {
29+
const text = await res.text();
30+
const err = new Error(message) as Error & {
31+
status?: number;
32+
response?: { status: number; data: string };
33+
};
34+
err.status = res.status;
35+
err.response = { status: res.status, data: text };
36+
throw err;
37+
}
38+
```
39+
40+
Tool handlers in `packages/stdio/src/index.ts` check `error.status`:
41+
```typescript
42+
function sanitizeError(error: unknown): string {
43+
const err = error as { status?: number; response?: { status: number; data: string }; message?: string } | null;
44+
if (err && typeof err === 'object' && typeof err.status === 'number') {
45+
const status = err.status;
46+
if (status === 401 || status === 403) return 'Authentication or permission error...';
47+
if (status === 404) return 'The requested content was not found...';
48+
// ...
49+
}
50+
}
51+
```
52+
53+
**Never reference `err.response.status` from axios** - use `err.status` directly.
54+
55+
### Tool Registration Pattern
56+
Use `McpServer.registerTool()` with Zod schemas. All tools wrapped in `toolHandler()` for consistent error handling:
57+
58+
```typescript
59+
// packages/stdio/src/index.ts
60+
server.registerTool(
61+
"get_page",
62+
{
63+
title: "Get Page Content",
64+
description: "Get full content of a specific page",
65+
inputSchema: {
66+
id: z.number().int().min(1).describe("Page ID")
67+
}
68+
},
69+
toolHandler(async (args) => {
70+
const page = await client.getPage(args.id);
71+
return {
72+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
73+
};
74+
})
75+
);
76+
```
77+
78+
### Write Operations Security
79+
Write tools only registered when `BOOKSTACK_ENABLE_WRITE=true`:
80+
81+
```typescript
82+
if (config.enableWrite) {
83+
server.registerTool("create_page", ...);
84+
server.registerTool("update_page", ...);
85+
}
86+
```
87+
88+
**Always check** `this.enableWrite` in BookStackClient before executing write methods.
89+
90+
### Response Enhancement
91+
All API responses are enhanced in `packages/core/src/bookstack-client.ts`:
92+
- Add direct URLs using slugs: `${baseUrl}/books/${book.slug}/page/${page.slug}`
93+
- Add markdown links, content previews (150-200 chars), word counts
94+
- Convert timestamps to human-friendly dates
95+
- Include contextual location info (book/chapter hierarchy)
96+
97+
Private methods: `enhanceBookResponse()`, `enhancePageResponse()`, `enhanceChapterResponse()`, `enhanceSearchResults()`.
98+
99+
### URL Generation Strategy
100+
- **Prefer slugs over IDs**: URLs use `book.slug` and `page.slug` when available
101+
- **Maintain caches**: `bookSlugCache` and `pageInfoCache` avoid redundant API calls
102+
- **Fallback**: If slug unavailable, use ID
103+
104+
## Development Workflows
105+
106+
### Build & Run
107+
```bash
108+
npm install # Install all workspace dependencies
109+
npm run build # Compile TypeScript in all packages
110+
npm run type-check # Type-check without emitting
111+
npm run dev # Hot reload (tsx on packages/stdio)
112+
npm start # Run production build
113+
```
114+
115+
### Testing
116+
```bash
117+
npm test # Run packages/core tests with vitest
118+
```
119+
120+
**Requirements**: Tests need environment variables:
121+
- `TEST_BOOKSTACK_URL`
122+
- `TEST_BOOKSTACK_TOKEN_ID`
123+
- `TEST_BOOKSTACK_TOKEN_SECRET`
124+
125+
Tests use `global-setup.ts` to seed data, `global-teardown.ts` to clean up. Sequential execution via vitest config. Helpers in `packages/core/tests/helpers.ts` provide `createWriteClient()`, `loadSeedData()`, etc.
126+
127+
### Adding a New Tool
128+
1. Add method to `BookStackClient` in `packages/core/src/bookstack-client.ts` if needed
129+
2. Register tool in `packages/stdio/src/index.ts` using `server.registerTool()`
130+
3. If write operation, add inside `if (config.enableWrite) { ... }` block
131+
4. Use `toolHandler()` wrapper for consistent error handling
132+
133+
### Modifying Enhancement Logic
134+
Edit private methods in `packages/core/src/bookstack-client.ts`:
135+
- `enhanceBookResponse()` - Add URLs, previews to books
136+
- `enhancePageResponse()` - Add URLs, word counts, previews to pages
137+
- `enhanceChapterResponse()` - Add URLs to chapters
138+
- `enhanceSearchResults()` - Add URLs, previews to search results
139+
140+
## Configuration
141+
142+
### Environment Variables (Required)
143+
```bash
144+
BOOKSTACK_BASE_URL=https://your-bookstack.com
145+
BOOKSTACK_TOKEN_ID=your-token-id
146+
BOOKSTACK_TOKEN_SECRET=your-token-secret
147+
BOOKSTACK_ENABLE_WRITE=false # Optional, defaults to false
148+
```
149+
150+
Validated in `packages/stdio/src/index.ts`:
151+
- `getRequiredEnvVar()` - Exits if missing
152+
- `validateBaseUrl()` - Checks URL format and scheme
153+
154+
### TypeScript Configuration
155+
- Target: ES2022 with ESNext modules
156+
- Module resolution: Bundler (for workspace imports)
157+
- Strict mode: disabled (historical)
158+
- Output: `dist/` in each package
159+
160+
## Deployment Options
161+
162+
### Claude Desktop
163+
Add to `claude_desktop_config.json`:
164+
```json
165+
{
166+
"mcpServers": {
167+
"bookstack": {
168+
"command": "node",
169+
"args": ["/path/to/packages/stdio/dist/index.js"],
170+
"env": { "BOOKSTACK_BASE_URL": "...", ... }
171+
}
172+
}
173+
}
174+
```
175+
176+
### LibreChat
177+
Add to `librechat.yaml`:
178+
```yaml
179+
mcpServers:
180+
bookstack:
181+
command: npx
182+
args: ["-y", "bookstack-mcp"]
183+
env:
184+
BOOKSTACK_BASE_URL: "https://..."
185+
```
186+
187+
## Common Gotchas
188+
189+
1. **Axios references**: This codebase uses native `fetch`, not axios. Error properties are `error.status` and `error.response`, not `error.response.status`.
190+
191+
2. **Workspace imports**: Use `@bookstack-mcp/core` to import from core package. TypeScript resolves via `exports` in `packages/core/package.json`.
192+
193+
3. **Stdio logging**: Use `console.error()` for logs (goes to stderr). Never use `console.log()` in `packages/stdio/src/index.ts` - it breaks stdio protocol.
194+
195+
4. **Write operations**: Always gate behind `config.enableWrite` check. Default is `false` for security.
196+
197+
5. **Response format**: All tool handlers return `{ content: [{ type: "text", text: string }] }`. Use `JSON.stringify(result, null, 2)` for readability.
198+
199+
6. **URL construction**: Always use slug-based URLs when available. Fallback to ID-based URLs. Never hardcode paths.
200+
201+
## Reference Files
202+
203+
- [packages/core/src/bookstack-client.ts](packages/core/src/bookstack-client.ts) - HTTP client, API methods, enhancement logic
204+
- [packages/stdio/src/index.ts](packages/stdio/src/index.ts) - Tool registration, error handling
205+
- [packages/core/src/types.ts](packages/core/src/types.ts) - Shared TypeScript types
206+
- [packages/core/tests/helpers.ts](packages/core/tests/helpers.ts) - Test utilities
207+
- [CLAUDE.md](CLAUDE.md) - Extended project documentation

.github/workflows/auto-tag.yml

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,25 @@
1-
name: Auto-tag on version bump
2-
3-
on:
4-
push:
5-
branches: [main]
6-
paths: [package.json]
7-
workflow_dispatch:
8-
9-
permissions:
10-
contents: write
11-
actions: write
12-
13-
jobs:
14-
auto-tag:
15-
runs-on: ubuntu-latest
16-
steps:
17-
- name: Checkout
18-
uses: actions/checkout@v4
19-
with:
20-
fetch-depth: 0
21-
22-
- name: Read version from package.json
23-
id: version
24-
run: echo "version=v$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
25-
26-
- name: Check if tag exists
27-
id: check
28-
run: |
29-
if git rev-parse "${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
30-
echo "exists=true" >> "$GITHUB_OUTPUT"
31-
else
32-
echo "exists=false" >> "$GITHUB_OUTPUT"
33-
fi
34-
35-
- name: Create tag
36-
if: steps.check.outputs.exists == 'false'
37-
run: |
38-
git tag "${{ steps.version.outputs.version }}"
39-
git push origin "${{ steps.version.outputs.version }}"
40-
41-
- name: Trigger Docker publish for tag
42-
run: |
43-
gh workflow run docker-publish.yml --ref "${{ steps.version.outputs.version }}"
44-
env:
45-
GH_TOKEN: ${{ github.token }}
1+
# auto-tag.yml is intentionally retired.
2+
#
3+
# Git tag creation has moved into docker-publish.yml (the "merge" job) so that
4+
# the git tag and the registry version tag are created atomically — only after
5+
# both arch images are verified in GHCR and the multi-arch manifest is confirmed
6+
# pullable.
7+
#
8+
# Previously this workflow:
9+
# 1. Read the version from the root package.json (wrong — root is private)
10+
# 2. Created a git tag
11+
# 3. Triggered docker-publish.yml via `gh workflow run`
12+
#
13+
# Problems that caused:
14+
# - Version sourced from private root package, not the published packages/stdio
15+
# - Docker publish was triggered even when the tag already existed
16+
# - Two Docker builds ran on every version bump (one from push-to-main, one
17+
# from the tag trigger), producing two registry pushes of the same image
18+
# - Git tag existed before images were verified, so tag and registry could
19+
# diverge on a failed publish
20+
#
21+
# The replacement flow (all in docker-publish.yml):
22+
# push to main → build & push amd64/arm64 → verify digests → merge manifest
23+
# → verify manifest pullable → create git tag → clean up staging tags
24+
#
25+
# This file is kept as documentation. It has no 'on:' trigger and will never run.

0 commit comments

Comments
 (0)