Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/mcp-server-instructions.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/release_checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions packages/mcp-server/src/instructions.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
33 changes: 33 additions & 0 deletions packages/mcp-server/src/instructions.ts
Original file line number Diff line number Diff line change
@@ -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(' ')
}
16 changes: 15 additions & 1 deletion packages/mcp-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)),
}
)

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/tools/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion specs/02-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading