diff --git a/.changeset/mcp-server-instructions.md b/.changeset/mcp-server-instructions.md new file mode 100644 index 0000000..1c43908 --- /dev/null +++ b/.changeset/mcp-server-instructions.md @@ -0,0 +1,7 @@ +--- +"@mtg-deckbuilder/shared": patch +"@mtg-deckbuilder/mcp-server": patch +"@mtg-deckbuilder/electron-app": patch +--- + +MCP server now sends `instructions` on initialize, telling the model to load the bundled mtg-deckbuilder skills when available (and to proceed with the tools when not). Also reports its real package version and fixes the `get_deck` description that pointed at the removed `view_deck` tool. diff --git a/.github/ISSUE_TEMPLATE/release_checklist.md b/.github/ISSUE_TEMPLATE/release_checklist.md index cf98665..b276e0c 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.md +++ b/.github/ISSUE_TEMPLATE/release_checklist.md @@ -31,7 +31,7 @@ assignees: "" - [ ] `list_decks` returns decks created in the desktop app - [ ] `create_deck` creates a deck visible in the desktop app - [ ] `add_card` resolves cards via Scryfall -- [ ] `view_deck` renders correctly +- [ ] `deck_list` renders correctly - [ ] `import_deck` imports a decklist ## Cross-App Sync diff --git a/packages/mcp-server/src/instructions.test.ts b/packages/mcp-server/src/instructions.test.ts new file mode 100644 index 0000000..1f53f55 --- /dev/null +++ b/packages/mcp-server/src/instructions.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { buildInstructions } from './instructions.js' + +const withSkills = { + skills_dir_configured: true, + skills: [ + { name: 'mtg-deckbuilder-decks', description: '', version: '1' }, + { name: 'scryfall-search', description: '', version: '1' }, + ], +} + +describe('buildInstructions', () => { + it('names each bundled skill when a skills dir is configured', () => { + const text = buildInstructions(withSkills) + expect(text).toContain('`mtg-deckbuilder-decks`') + expect(text).toContain('`scryfall-search`') + }) + + it('keeps the install nudge and tool guidance without a skills dir', () => { + const text = buildInstructions({ skills_dir_configured: false, skills: [] }) + expect(text).toContain('Settings → Skills') + expect(text).toContain('do not wait for it') + expect(text).toContain('`deck_list`') + expect(text).not.toContain('This server bundles') + }) + + it('conditions skill loading on availability', () => { + expect(buildInstructions(withSkills)).toContain('if it appears in your available skills') + }) +}) diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 0000000..7dc8af4 --- /dev/null +++ b/packages/mcp-server/src/instructions.ts @@ -0,0 +1,33 @@ +import type { ListBundledSkillsResult } from './tools/skill-tools.js' + +// --------------------------------------------------------------------------- +// Server `instructions`, sent once in the MCP `initialize` response and +// injected by clients into the model's context for the whole session. +// +// Every sentence about skills is conditional ("if available"): skills are +// installed by the Electron app, so the model must never block on, invoke, or +// guess at a skill it doesn't have. The tool guidance stands on its own. +// --------------------------------------------------------------------------- + +const INSTALL_NUDGE = + 'If a skill is not in your list, do not try to invoke it and do not wait for it — the tools work without skills. ' + + 'Mention once that they ship with the MTG Deckbuilder app (Settings → Skills → Install all for Claude Code / Gemini CLI / Codex, ' + + 'or Export the zip for Claude Desktop → Capabilities), then proceed with the tools.' + +const TOOL_GUIDANCE = + 'Card sets are `mainboard | sideboard | alternates | cut`; `deck_export` calls `alternates` "maybeboard". ' + + 'Start deck-analysis tasks with `deck_list`.' + +function skillList(skills: ListBundledSkillsResult): string { + return skills.skills.map(s => `\`${s.name}\``).join(', ') +} + +function skillsSentence(skills: ListBundledSkillsResult): string { + const base = 'Before any deck, list, or card-search task, load the matching mtg-deckbuilder skill **if it appears in your available skills**' + if (!skills.skills_dir_configured || skills.skills.length === 0) return `${base}.` + return `${base}. This server bundles: ${skillList(skills)}; \`list_bundled_skills\` shows descriptions and versions.` +} + +export function buildInstructions(skills: ListBundledSkillsResult): string { + return [skillsSentence(skills), INSTALL_NUDGE, TOOL_GUIDANCE].join(' ') +} diff --git a/packages/mcp-server/src/main.ts b/packages/mcp-server/src/main.ts index 160019e..108f489 100644 --- a/packages/mcp-server/src/main.ts +++ b/packages/mcp-server/src/main.ts @@ -6,9 +6,22 @@ import { ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js' +import * as fs from 'fs' import * as path from 'path' import { Storage, loadCardDeckLimits } from '@mtg-deckbuilder/shared' import { handleToolCall, getToolDefinitions } from './tools/index.js' +import { listBundledSkills } from './tools/skill-tools.js' +import { buildInstructions } from './instructions.js' + +// Both dist/main.js and dist/bundle.js sit one level below package.json. +function readPackageVersion(): string { + try { + const pkgPath = new URL('../package.json', import.meta.url) + return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? 'unknown' + } catch { + return 'unknown' + } +} function parseFlag(argv: string[], flag: string): string | undefined { const i = argv.indexOf(flag) @@ -29,12 +42,13 @@ async function main() { const server = new Server( { name: 'mtg-deckbuilder-mcp', - version: '0.1.0', + version: readPackageVersion(), }, { capabilities: { tools: {}, }, + instructions: buildInstructions(listBundledSkills(skillsDir)), } ) diff --git a/packages/mcp-server/src/tools/schemas.ts b/packages/mcp-server/src/tools/schemas.ts index 50b18f0..a0a0a6c 100644 --- a/packages/mcp-server/src/tools/schemas.ts +++ b/packages/mcp-server/src/tools/schemas.ts @@ -30,7 +30,7 @@ export function getToolDefinitions(): Tool[] { }, { name: 'get_deck', - description: 'Returns raw deck JSON with Scryfall IDs, metadata, and format validation. Use for programmatic operations or exporting. For deck evaluation/analysis, use view_deck instead.', + description: 'Returns raw deck JSON with Scryfall IDs, metadata, and format validation. Use for programmatic operations or exporting. For deck evaluation/analysis, use deck_list instead.', inputSchema: { type: 'object', properties: { diff --git a/specs/02-mcp-server.md b/specs/02-mcp-server.md index d376830..ef0a70c 100644 --- a/specs/02-mcp-server.md +++ b/specs/02-mcp-server.md @@ -25,7 +25,7 @@ Returns summary info for every saved deck. ### `get_deck` -Returns the raw deck JSON (with Scryfall IDs, metadata, and a structural validation report). Use this for programmatic operations or exports. For human-readable deck analysis, use `view_deck` instead. +Returns the raw deck JSON (with Scryfall IDs, metadata, and a structural validation report). Use this for programmatic operations or exports. For human-readable deck analysis, use `deck_list` instead. **Input:** - `identifier` *(string, required)* — deck UUID or name (case-insensitive).