Merge pull request #124 from evdboom/chatgptwork-support

Chatgptwork support
This commit is contained in:
Erik van der Boom 2026-07-14 22:53:26 +02:00 committed by GitHub
commit 6367d9563f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 212 additions and 153 deletions

24
.github/release-notes-template.md vendored Normal file
View file

@ -0,0 +1,24 @@
# Bindery {{VERSION}}
## Highlights
- Standardized release packaging across all supported surfaces.
- See attached assets below for install-ready downloads.
## Release Assets
- bindery-{{VERSION}}.mcpb (Claude Desktop / Cowork MCP package)
- bindery-mcp-server-{{VERSION}}.zip (Standalone MCP package for clients that support command + args + env configuration, such as ChatGPT Work and LM Studio)
- bindery-{{VERSION}}.vsix (VS Code extension)
- bindery-obsidian-bundled-{{VERSION}}.zip (Obsidian plugin bundle)
- obsidian-plugin/dist/main.js and obsidian-plugin/dist/manifest.json (raw Obsidian artifacts for obsidian automated scanning)
## Installation
- VS Code extension and MCP setup overview: https://github.com/evdboom/Bindery/blob/main/README.md
- MCP package details and manual install: https://github.com/evdboom/Bindery/blob/main/mcpb/README.md
- Standalone MCP zip quick install: included README.md inside bindery-mcp-server-{{VERSION}}.zip
## Notes
- Release notes below this section are auto-generated from merged pull requests and commits.

View file

@ -25,9 +25,9 @@ jobs:
fi
echo "release_version=$VERSION" >> "$GITHUB_OUTPUT"
# ── Job 1: Build .mcpb for Claude Desktop ───────────────────────────────────
# ── Job 1: Build .mcpb for Claude Desktop + standalone MCP zip ─────────────
build-mcpb:
name: Build .mcpb
name: Build MCP Packages
needs: validate-release-version
runs-on: ubuntu-latest
permissions:
@ -76,15 +76,38 @@ jobs:
mv "$f" "bindery-${VERSION}.mcpb" 2>/dev/null || true
done
- name: Package standalone MCP server zip
run: |
VERSION="$RELEASE_VERSION"
mkdir -p mcpb/standalone-mcp/server
cp mcpb/server/index.js mcpb/standalone-mcp/server/index.js
cp mcpb/standalone-mcp-README.md mcpb/standalone-mcp/README.md
cd mcpb/standalone-mcp
zip "../../bindery-mcp-server-${VERSION}.zip" README.md server/index.js
- name: Render standardized release notes
run: |
VERSION="$RELEASE_VERSION"
sed "s/{{VERSION}}/${VERSION}/g" .github/release-notes-template.md > .github/release-notes.generated.md
- name: Attest .mcpb build provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: mcpb/*.mcpb
- name: Attest standalone MCP zip provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: bindery-mcp-server-*.zip
- name: Upload .mcpb to release
uses: softprops/action-gh-release@v3
with:
files: mcpb/*.mcpb
files: |
mcpb/*.mcpb
bindery-mcp-server-*.zip
body_path: .github/release-notes.generated.md
generate_release_notes: true
fail_on_unmatched_files: true
# ── Job 2: Package VS Code extension + attach VSIX to release ───────────────

View file

@ -85,7 +85,10 @@ Then in Obsidian: Settings → Community plugins (if not restricted) → Install
### [mcpb/](mcpb/) — Claude Desktop Extension
Packages the MCP server as a `.mcpb` file for one-click installation in Claude Desktop or Cowork.
Release assets now include two MCP package formats:
- `bindery-<version>.mcpb` for Claude Desktop / Cowork
- `bindery-mcp-server-<version>.zip` as a standalone MCP package for clients that support command + args + env setup (for example ChatGPT Work or LM Studio)
**Download the latest release** from [Releases](../../releases) — no build step needed.
@ -110,6 +113,21 @@ Packages the MCP server as a `.mcpb` file for one-click installation in Claude D
- **Note:** full embedding can be a heavy operation, depending on your hardware, when running a local Ollama instance.
6. Tools are now available — the agent calls `list_books` to discover book names
### Standalone MCP Clients (ChatGPT Work, LM Studio, etc.)
1. Download `bindery-mcp-server-*.zip` from the [latest release](../../releases/latest)
2. Unzip it to a stable folder
3. In your MCP client, add an MCP server with:
- eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab
- Command: `node`
- Args: absolute path to `server/index.js` from the unzipped folder
4. Set environment variables:
- `BINDERY_BOOKS` (required): semicolon-separated `Name=path` pairs
- `BINDERY_OLLAMA_URL` (optional): Ollama endpoint for semantic features
- `BINDERY_ENABLE_SEMANTIC_INDEX` (optional): `true` to enable full semantic index builds
- `BINDERY_DEFAULT_SEARCH_MODE` (optional): `lexical`, `semantic_rerank`, or `full_semantic`
5. Save and reconnect the MCP server
## Architecture Overview
The following flow shows how Bindery's writing environments connect to agents, MCP, and skills at a high level.

View file

@ -2,7 +2,7 @@ import type { TemplateContext, TemplateMeta } from './context';
export const meta: TemplateMeta = {
file: '.bindery/README.md',
version: 9,
version: 10,
label: 'bindery capabilities',
zip: null,
};
@ -121,7 +121,7 @@ tagged **(writes)** modify files or git state.
### Tool Workflow Shortcuts
- Use \`init_workspace\` first for a new book. It creates the settings, translation file, generated capability README, and the default authoring scaffold.
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, Claude skills, skill zips, and this capability README.
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, Claude skills, and this capability README.
- Use \`arc_*\` for story architecture under \`${arcFolder}/\`.
- Use \`character_*\` for cast profiles under \`${charactersFolder}/\`.
- Use \`note_*\` for canonical story notes under \`${notesFolder}/\`.

View file

@ -2,7 +2,7 @@ import { audienceNote, languageSection, type TemplateContext, type TemplateMeta
export const meta: TemplateMeta = {
file: 'CLAUDE.md',
version: 15,
version: 17,
label: 'project instructions',
zip: null,
};
@ -23,7 +23,7 @@ export function render(ctx: TemplateContext): string {
'## Start of session',
'1. Use /read-in at the start of a session to load context and get your bearings.',
'2. Run `health` from the Bindery MCP and check `ai_versions_outdated`.',
'3. If `ai_versions_outdated` has entries, run `setup_ai_files` and present the returned `skill_zips.reupload_required` list to the user for Claude Desktop.',
'3. If `ai_versions_outdated` has entries, run `setup_ai_files`; if `skill_files.reupload_required` has entries, ask the user to re-upload those SKILL.md files in Claude Desktop Skills (no zip needed).',
`4. If the skill or MCP server is not available, read at least ${sessionFile} (if present) for current focus and handoff context, and ${preferencesFile} for the author's durable working preferences.`,
'',
'## Memory system',
@ -82,7 +82,7 @@ export function render(ctx: TemplateContext): string {
'| `identify_book` | Match a working directory to a book name |',
'| `health` | Server status: settings, index, embedding backend |',
'| `init_workspace` | Create or update `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |',
'| `setup_ai_files` | Regenerate AI instruction files, rebuild Claude skill zip files, and return a change manifest |',
'| `setup_ai_files` | Regenerate AI instruction files and return a change manifest |',
'| `index_build` | Build or rebuild the full-text search index |',
'| `index_status` | Show index chunk count and build time |',
'| `get_text` | Read any file by relative path, with optional line range |',

View file

@ -24,7 +24,7 @@ export interface TemplateContext {
hasMultiLang: boolean;
}
/** Per-template metadata. `zip` is non-null only for skills (which ship as zips). */
/** Per-template metadata used by AI file version reporting. */
export interface TemplateMeta {
/** Output path relative to the workspace root. Used as the FILE_VERSION_INFO key. */
file: string;
@ -32,7 +32,7 @@ export interface TemplateMeta {
version: number;
/** Short, human-readable label used by health reporting. */
label: string;
/** Companion zip path (skills only) or null. */
/** Legacy companion zip path or null. */
zip: string | null;
}

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/brainstorm/SKILL.md',
version: 12,
label: 'brainstorm skill',
zip: '.claude/skills/brainstorm.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/character-setup/SKILL.md',
version: 1,
label: 'character-setup skill',
zip: '.claude/skills/character-setup.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/continuity/SKILL.md',
version: 14,
label: 'continuity skill',
zip: '.claude/skills/continuity.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/memory/SKILL.md',
version: 14,
label: 'memory skill',
zip: '.claude/skills/memory.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/plan-beats/SKILL.md',
version: 3,
label: 'plan-beats skill',
zip: '.claude/skills/plan-beats.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/proof-read/SKILL.md',
version: 6,
label: 'proof-read skill',
zip: '.claude/skills/proof-read.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/read-aloud/SKILL.md',
version: 11,
label: 'read-aloud skill',
zip: '.claude/skills/read-aloud.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/read-in/SKILL.md',
version: 16,
label: 'read-in skill',
zip: '.claude/skills/read-in.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/review/SKILL.md',
version: 14,
label: 'review skill',
zip: '.claude/skills/review.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/status/SKILL.md',
version: 14,
label: 'status skill',
zip: '.claude/skills/status.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/translate/SKILL.md',
version: 10,
label: 'translate skill',
zip: '.claude/skills/translate.zip',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,7 @@ export const meta: TemplateMeta = {
file: '.claude/skills/translation-review/SKILL.md',
version: 4,
label: 'translation-review skill',
zip: '.claude/skills/translation-review.zip',
zip: null,
};
const CONTENT = [

View file

@ -12,7 +12,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { zipSync, strToU8 } from 'fflate';
import {
renderTemplate,
FILE_VERSION_INFO,
@ -161,7 +160,6 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
const skillMd = path.join('.claude', 'skills', skill, 'SKILL.md');
writeFile(root, skillMd, renderTemplate(skill, ctx), overwrite, versionFile, result);
}
rebuildSkillZips(root, skills, result);
break;
case 'copilot':
writeFile(root, path.join('.github', 'copilot-instructions.md'), renderTemplate('copilot', ctx), overwrite, versionFile, result);
@ -279,60 +277,6 @@ function stampAiVersionFile(root: string, versionFile: AiVersionFile): void {
);
}
function rebuildSkillZips(root: string, skills: SkillTemplate[], result: AiSetupResult): void {
for (const skill of skills) {
const skillDir = path.join(root, '.claude', 'skills', skill);
const skillMd = path.join(skillDir, 'SKILL.md');
if (!fs.existsSync(skillMd)) { continue; }
const zipRel = `.claude/skills/${skill}.zip`;
const zipAbs = path.join(root, zipRel);
const zipExists = fs.existsSync(zipAbs);
const upToDate = zipExists
&& fs.statSync(zipAbs).mtimeMs >= fs.statSync(skillMd).mtimeMs;
if (upToDate) {
result.skillZipManifest.skipped.push(zipRel);
continue;
}
if (!zipSkillFolder(root, skill, zipAbs)) {
result.skillZipManifest.failed.push(zipRel);
continue;
}
if (zipExists) {
result.skillZipManifest.rebuilt.push(zipRel);
} else {
result.skillZipManifest.created.push(zipRel);
}
}
}
function toKey(relPath: string): string {
return relPath.replaceAll('\\', '/');
}
function zipSkillFolder(root: string, skill: SkillTemplate, zipAbs: string): boolean {
const skillMd = path.join(root, '.claude', 'skills', skill, 'SKILL.md');
if (!fs.existsSync(skillMd)) { return false; }
try {
const skillContent = fs.readFileSync(skillMd, 'utf-8');
const zipBytes = zipSync({
[`${skill}/SKILL.md`]: strToU8(skillContent),
});
fs.mkdirSync(path.dirname(zipAbs), { recursive: true });
const copyPath = zipAbs + '.tmp';
fs.writeFileSync(copyPath, Buffer.from(zipBytes));
if (fs.existsSync(zipAbs)) {
fs.unlinkSync(zipAbs);
}
fs.renameSync(copyPath, zipAbs);
return true;
} catch {
return false;
}
}

View file

@ -616,7 +616,7 @@ server.registerTool('setup_ai_files', {
title: 'Setup AI Files',
description:
'Generate AI assistant instruction files (CLAUDE.md, .github/copilot-instructions.md, ' +
'.cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from .bindery/settings.json. ' +
'.cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from .bindery/settings.json. ' +
'Run init_workspace first. Safe to run multiple times — skips existing files unless overwrite is true.',
inputSchema: {
book: bookSchema,

View file

@ -518,7 +518,7 @@ export function toolHealth(root: string): string {
ai_version_outdated: aiVersionsOutdated.length > 0,
ai_versions_outdated: aiVersionsOutdated,
message: aiVersionsOutdated.length > 0
? 'AI instruction files are out of date. Run setup_ai_files, then re-upload any listed skill zip files in Claude Desktop.'
? 'AI instruction files are out of date. Run setup_ai_files to regenerate them. If you use Claude Desktop skills, re-upload updated SKILL.md files.'
: 'AI instruction files are up to date.',
};
@ -2621,10 +2621,7 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string {
}
} catch { /* non-fatal */ }
const zipToUpload = [
...result.skillZipManifest.created,
...result.skillZipManifest.rebuilt,
];
const skillFilesToReupload = result.regenerated.filter(file => /^\.claude\/skills\/[^/]+\/SKILL\.md$/.test(file));
const response = {
regenerated_files: result.regenerated,
@ -2634,12 +2631,15 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string {
rebuilt: result.skillZipManifest.rebuilt,
skipped: result.skillZipManifest.skipped,
failed: result.skillZipManifest.failed,
reupload_required: zipToUpload,
reupload_required: [],
},
skill_files: {
reupload_required: skillFilesToReupload,
},
ai_versions: result.versionStamp,
message:
'If you are using Claude Desktop as AI assistant, re-upload these skill zips via Customize -> Skills: ' +
(zipToUpload.length > 0 ? zipToUpload.join(', ') : 'none'),
message: skillFilesToReupload.length > 0
? 'AI instruction files were generated. If you use Claude Desktop skills, re-upload these SKILL.md files in Claude Desktop: ' + skillFilesToReupload.join(', ')
: 'AI instruction files were generated. If you use Claude Desktop skills, upload the generated SKILL.md files in Claude Desktop',
};
return JSON.stringify(response, null, 2);

View file

@ -21,8 +21,6 @@ async function loadAiSetup() {
return import('../src/aisetup');
}
vi.unmock('fflate');
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
@ -31,8 +29,8 @@ afterEach(() => {
}
});
describe('setupAiFiles zip generation', () => {
it('builds the translation-review skill zip when requested', async () => {
describe('setupAiFiles skill generation', () => {
it('generates requested skill markdown files for claude target', async () => {
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
@ -49,11 +47,12 @@ describe('setupAiFiles zip generation', () => {
overwrite: true,
});
expect(result.skillZipManifest.created).toContain('.claude/skills/translation-review.zip');
expect(result.regenerated).toContain('.claude/skills/translation-review/SKILL.md');
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review', 'SKILL.md'))).toBe(true);
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review.zip'))).toBe(false);
});
it('builds skill zips in-process with normalized entry paths', async () => {
it('returns an empty skill zip manifest (zips are no longer generated)', async () => {
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
@ -70,45 +69,33 @@ describe('setupAiFiles zip generation', () => {
overwrite: true,
});
expect(result.skillZipManifest.created).toContain('.claude/skills/read-aloud.zip');
expect(result.skillZipManifest.failed).toEqual([]);
const zipPath = path.join(root, '.claude', 'skills', 'read-aloud.zip');
expect(fs.existsSync(zipPath)).toBe(true);
const zipText = fs.readFileSync(zipPath, 'latin1');
expect(zipText).toContain('read-aloud/SKILL.md');
expect(zipText).not.toContain('read-aloud\\SKILL.md');
});
it('reports a failed skill zip when archive generation throws', async () => {
vi.doMock('fflate', async () => {
const actual = await vi.importActual<typeof import('fflate')>('fflate');
return {
...actual,
zipSync: () => {
throw new Error('zip failed');
},
};
});
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
const result = setupAiFiles({
root,
targets: ['claude'],
skills: ['read-aloud'],
overwrite: true,
});
expect(result.skillZipManifest.failed).toContain('.claude/skills/read-aloud.zip');
expect(result.skillZipManifest.created).toEqual([]);
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'read-aloud.zip'))).toBe(false);
expect(result.skillZipManifest.rebuilt).toEqual([]);
expect(result.skillZipManifest.skipped).toEqual([]);
expect(result.skillZipManifest.failed).toEqual([]);
});
it('does not overwrite legacy zip files that already exist', async () => {
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
write(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'legacy zip bytes');
const result = setupAiFiles({
root,
targets: ['claude'],
skills: ['read-aloud'],
overwrite: true,
});
expect(result.skillZipManifest.failed).toEqual([]);
expect(result.skillZipManifest.created).toEqual([]);
expect(fs.readFileSync(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'utf-8')).toBe('legacy zip bytes');
});
});

View file

@ -343,6 +343,7 @@ describe('mcp tools', () => {
regenerated_files?: string[];
skipped_files?: string[];
skill_zips?: { created?: string[]; rebuilt?: string[]; skipped?: string[]; failed?: string[]; reupload_required?: string[] };
skill_files?: { reupload_required?: string[] };
ai_versions?: { versions?: Record<string, { version: number; label: string; zip: string | null }> };
};
@ -353,10 +354,12 @@ describe('mcp tools', () => {
expect(Array.isArray(parsed.skill_zips?.skipped)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.failed)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.reupload_required)).toBe(true);
expect(Array.isArray(parsed.skill_files?.reupload_required)).toBe(true);
expect(parsed.skill_files?.reupload_required).toContain('.claude/skills/review/SKILL.md');
expect(parsed.ai_versions?.versions?.['.claude/skills/review/SKILL.md']?.label).toBe('review skill');
});
it('builds skill zips with normalized forward-slash entry paths', () => {
it('does not build skill zip artifacts', () => {
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
@ -364,14 +367,19 @@ describe('mcp tools', () => {
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
toolSetupAiFiles(root, { targets: ['claude'], skills: ['read-aloud'], overwrite: true });
const raw = toolSetupAiFiles(root, { targets: ['claude'], skills: ['read-aloud'], overwrite: true });
const parsed = JSON.parse(raw) as {
skill_zips?: { created?: string[]; rebuilt?: string[]; failed?: string[]; reupload_required?: string[] };
skill_files?: { reupload_required?: string[] };
};
const zipPath = path.join(root, '.claude', 'skills', 'read-aloud.zip');
expect(fs.existsSync(zipPath)).toBe(true);
const zipText = fs.readFileSync(zipPath).toString('latin1');
expect(zipText).toContain('read-aloud/SKILL.md');
expect(zipText).not.toContain(String.raw`read-aloud\SKILL.md`);
expect(fs.existsSync(zipPath)).toBe(false);
expect(parsed.skill_zips?.created).toEqual([]);
expect(parsed.skill_zips?.rebuilt).toEqual([]);
expect(parsed.skill_zips?.failed).toEqual([]);
expect(parsed.skill_zips?.reupload_required).toEqual([]);
expect(parsed.skill_files?.reupload_required).toEqual(['.claude/skills/read-aloud/SKILL.md']);
});
it('health skips claude files when aiTargets excludes claude', () => {

View file

@ -1,4 +1,4 @@
# Bindery MCP — Desktop Extension (.mcpb)
# Bindery MCP — Desktop Packages
Book authoring tools for Claude Desktop: chapter navigation, full-text search,
translation management, session memory, typography formatting, and version snapshots.
@ -20,7 +20,7 @@ Works with any Markdown book project structured with the Bindery VS Code extensi
- **Version snapshots** — git-based save points after writing sessions
- **Review diffs** — structured git diff of uncommitted changes
## Manual Installation
## Manual Installation (Claude Desktop)
To install manually without using published Claude Connectors
@ -29,6 +29,21 @@ To install manually without using published Claude Connectors
3. Click **Install from file** (or drag-drop the `.mcpb` file)
4. Fill in the **Books** field (see Configuration below)
## Manual Installation (Standalone MCP clients)
1. Download the `bindery-mcp-server-*.zip` file from the latest release
2. Unzip it to a stable folder
3. In your MCP client, add a new MCP server.
- eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab
4. With:
- Command: `node`
- Args: absolute path to `server/index.js` from the unzipped folder
5. Add environment variables:
- `BINDERY_BOOKS` (required): semicolon-separated `Name=path` pairs
- `BINDERY_OLLAMA_URL` (optional): Ollama endpoint for semantic features
- `BINDERY_ENABLE_SEMANTIC_INDEX` (optional): `true` to enable full semantic index builds
- `BINDERY_DEFAULT_SEARCH_MODE` (optional): `lexical`, `semantic_rerank`, or `full_semantic`
## Configuration
| Setting | Required | Description |
@ -165,7 +180,7 @@ table with issue type, location, and the reference that contradicts it.
| `identify_book` | Match a working directory to a book name |
| `health` | Server status: settings, index, embedding backend |
| `init_workspace` | Create or update `.bindery/settings.json`, `translations.json`, generated `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |
| `setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and refresh generated `.bindery/README.md` |
| `setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and refresh generated `.bindery/README.md` |
| `index_build` | Build or rebuild the lexical index and, when enabled, the semantic embedding index |
| `index_status` | Show lexical and semantic index status, build times, and stale hints |
| `get_text` | Read any file by relative path, with optional line range |
@ -230,6 +245,8 @@ mcpb pack
This produces `bindery-mcp-<version>.mcpb`.
For standalone MCP clients, release CI also produces `bindery-mcp-server-<version>.zip` containing `server/index.js` and setup instructions.
## Files
- `manifest.json` — Extension metadata, tool declarations, user config schema

View file

@ -62,7 +62,7 @@
{ "name": "identify_book", "description": "Identify which book matches a working directory. Pass your cwd and the server matches by folder name or .bindery/settings.json." },
{ "name": "health", "description": "Check server status: active book, settings, search index, and embedding backend." },
{ "name": "init_workspace", "description": "Create or update .bindery/settings.json, translations.json, .bindery/README.md, and the opinionated Arc, Notes, Characters, SESSION, PREFERENCES, memory, and chapter-status scaffold. All arguments optional — smart defaults used if omitted. Detects language folders automatically. Safe to run on an existing workspace." },
{ "name": "setup_ai_files", "description": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from settings. Run init_workspace first. Skips existing files unless overwrite: true." },
{ "name": "setup_ai_files", "description": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from settings. Run init_workspace first. Skips existing files unless overwrite: true." },
{ "name": "index_build", "description": "Build or rebuild the lexical search index and, when enabled, the semantic embedding index for a book." },
{ "name": "index_status", "description": "Show lexical and semantic index metadata, including stale-status hints." },
{ "name": "search", "description": "Search using lexical BM25, semantic reranking, or full semantic mode. Semantic modes fall back to lexical with a warning if Ollama or the semantic index is unavailable." },

View file

@ -0,0 +1,38 @@
# Bindery Standalone MCP Server Package
This package contains:
- `server/index.js` (the bundled Bindery MCP server)
- `README.md` (this file)
## Install in any MCP client
1. Unzip this archive to a stable folder on your machine.
2. In your MCP client, add a new MCP server.
- eg. in ChatGPT Work -> **Settings** -> **Plug-ins** -> **MCP's** tab
3. Use the following values:
- Command: `node`
- Args: absolute path to `server/index.js` in the folder you unzipped
4. Configure environment variables (at minimum `BINDERY_BOOKS`).
## Environment variables
- `BINDERY_BOOKS` (required)
- Semicolon-separated `Name=path` pairs.
- Example: `MyBook=C:\Users\Me\MyBook;MyNovel=D:\Writing\MyNovel`
- `BINDERY_OLLAMA_URL` (optional)
- Ollama endpoint for semantic reranking/full semantic search.
- Example: `http://localhost:11434`
- `BINDERY_ENABLE_SEMANTIC_INDEX` (optional)
- `true` to enable full semantic index builds via `index_build`.
- Default: `false`
- `BINDERY_DEFAULT_SEARCH_MODE` (optional)
- One of `lexical`, `semantic_rerank`, `full_semantic`.
- Default: `lexical`
## Notes
- Keep the unzipped folder in place after configuring the MCP server.
- If Node is not on PATH, use the absolute path to the Node executable as Command.
- If Node is not installed at all you can get it from https://nodejs.org/
- This package is suitable for any MCP client that supports executable command + arguments + environment variables (for example ChatGPT Work or LM Studio).

View file

@ -126,7 +126,7 @@ describe('AI Setup', () => {
expect(original).not.toBe(updated);
});
it('should create skill zips for requested skills', async () => {
it('should create skill markdown files for requested skills', async () => {
const settingsPath = path.join(tempRoot, '.bindery', 'settings.json');
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify({

View file

@ -66,7 +66,7 @@ Bindery includes a bundled MCP server that makes your book's chapters, arc files
|------|-------------|
| `bindery_health` | Check workspace status: settings, search index, and embedding backend |
| `bindery_init_workspace` | Create or update `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |
| `bindery_setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, etc.), Claude skills, skill zips, and refresh generated `.bindery/README.md` |
| `bindery_setup_ai_files` | Generate AI instruction files (CLAUDE.md, copilot-instructions.md, etc.), Claude skills, and refresh generated `.bindery/README.md` |
| `bindery_settings_update` | Merge a partial patch into `.bindery/settings.json` |
| `bindery_index_build` | Build or rebuild the search index (lexical + optional semantic) |
| `bindery_index_status` | Show index metadata and stale-status hints |
@ -138,7 +138,7 @@ All commands are available from the Command Palette (`Ctrl+Shift+P`) under the *
| Command | Description |
|---------|-------------|
| `Initialize Workspace` | Create `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |
| `Setup AI Assistant Files` | Generate CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md, Claude skills, skill zips, and refresh generated `.bindery/README.md` |
| `Setup AI Assistant Files` | Generate CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md, Claude skills, and refresh generated `.bindery/README.md` |
| `Register MCP Server` | Write `.vscode/mcp.json` for Claude / Codex MCP discovery |
| `Format Typography` | Apply typography formatting to the active markdown file (`Ctrl+K Ctrl+B`) |
| `Format All Markdown in Folder` | Apply typography to all `.md` files in a folder |

View file

@ -1057,7 +1057,7 @@
"displayName": "Bindery: Setup AI Files",
"toolReferenceName": "binderySetupAiFiles",
"canBeReferencedInPrompt": true,
"modelDescription": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, skill zips, and the generated .bindery/README.md capability reference from .bindery/settings.json. Run init_workspace first. Skips existing files unless overwrite is true.",
"modelDescription": "Generate AI assistant instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md), Claude skill templates, and the generated .bindery/README.md capability reference from .bindery/settings.json. Run init_workspace first. Skips existing files unless overwrite is true.",
"inputSchema": {
"type": "object",
"properties": {

View file

@ -1163,12 +1163,12 @@ async function setupAiCommand(context?: vscode.ExtensionContext) {
const parsed = JSON.parse(raw) as {
regenerated_files?: string[];
skipped_files?: string[];
skill_zips?: { reupload_required?: string[] };
skill_files?: { reupload_required?: string[] };
};
const regenerated = parsed.regenerated_files ?? [];
const skipped = parsed.skipped_files ?? [];
const reupload = parsed.skill_zips?.reupload_required ?? [];
const reuploadSkillFiles = parsed.skill_files?.reupload_required ?? [];
const summary: string[] = [];
if (regenerated.length > 0) {
@ -1179,8 +1179,8 @@ async function setupAiCommand(context?: vscode.ExtensionContext) {
}
const base = summary.length > 0 ? summary.join(' | ') : 'No files changed.';
const suffix = reupload.length > 0
? ` If you use Claude Desktop: open Customize > Skills and re-upload ${reupload.join(', ')}.`
const suffix = reuploadSkillFiles.length > 0
? ` If you use Claude Desktop skills, re-upload these SKILL.md files: ${reuploadSkillFiles.join(', ')}.`
: '';
vscode.window.showInformationMessage(base + suffix);
} catch {