← スキル一覧に戻る

spine-mcp-tools
by cr8or-space
A tool for producing books and stories at scale using LLM assistance.
⭐ 0🍴 0📅 2026年1月8日
SKILL.md
name: spine-mcp-tools description: "Guide for creating MCP (Model Context Protocol) tools for Spine domains. Covers tool naming, parameter patterns, session context, error handling, and testing."
Spine MCP Tool Development
Overview
Each Spine domain exposes its functionality as MCP tools. MCP enables AI assistants like Claude to interact with Spine projects programmatically.
Architecture
Claude Desktop / MCP Client
↓
MCP Protocol (stdio)
↓
Domain MCP Server (packages/{domain}/mcp)
↓
WebSocket Client
↓
Spine Server (domain handlers)
↓
Domain Core Logic
The MCP server is a thin layer that translates MCP tool calls into WebSocket RPC calls.
Package Structure
packages/{domain}/mcp/
├── package.json
└── src/
├── index.ts # Entry point, server setup
├── config.ts # Configuration loading
├── client.ts # WebSocket client wrapper
└── tools/
├── index.ts # Tool registration
├── project.ts # Project tools
├── bible.ts # Bible tools (serial)
├── concept.ts # Concept tools (techbook)
└── ...
Tool Naming Convention
spine_{domain_resource}_{action}
Pattern breakdown:
spine_— Prefix for all Spine tools{domain_resource}— Domain and resource, snake_case{action}— What it does
Examples:
spine_project_list
spine_project_create
spine_project_load
spine_bible_character_create # serial domain
spine_bible_character_list
spine_bible_location_create
spine_concept_create # techbook domain
spine_concept_deps
spine_snippet_create
spine_checkpoint_validate
Actions:
list— Get all resourcesget— Get single resourcecreate— Create new resourceupdate— Update resourcedelete— Delete resource (requires confirm)tree— Get hierarchical viewstart/stop/status— For processes
Tool Definition
Basic Structure
// packages/serial/mcp/src/tools/bible.ts
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { SpineClient } from '../client.js';
export function createBibleTools(client: SpineClient): Tool[] {
return [
{
name: 'spine_bible_character_create',
description: 'Create a new character in the story bible',
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'Project ID (uses current project if not specified)',
},
name: {
type: 'string',
description: 'Character name',
},
role: {
type: 'string',
enum: ['protagonist', 'antagonist', 'supporting', 'minor'],
description: 'Character role in the story',
},
traits: {
type: 'array',
items: { type: 'string' },
description: 'Character personality traits',
},
description: {
type: 'string',
description: 'Character description',
},
},
required: ['name', 'role'],
},
},
// ... more tools
];
}
Tool Handler
// packages/serial/mcp/src/tools/handlers.ts
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { SpineClient } from '../client.js';
import { SessionState } from '../session.js';
export async function handleToolCall(
name: string,
args: Record<string, unknown>,
client: SpineClient,
session: SessionState
): Promise<CallToolResult> {
switch (name) {
case 'spine_bible_character_create':
return handleCharacterCreate(args, client, session);
case 'spine_bible_character_list':
return handleCharacterList(args, client, session);
// ... more handlers
default:
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
}
}
async function handleCharacterCreate(
args: Record<string, unknown>,
client: SpineClient,
session: SessionState
): Promise<CallToolResult> {
// Get project ID from args or session
const projectId = (args.projectId as string) ?? session.currentProjectId;
if (!projectId) {
return {
content: [{ type: 'text', text: 'No project loaded. Use spine_project_load first.' }],
isError: true,
};
}
try {
const result = await client.call('serial.bible.character.create', {
projectId,
name: args.name,
role: args.role,
traits: args.traits ?? [],
description: args.description ?? '',
});
return {
content: [{
type: 'text',
text: formatCharacterResult(result),
}],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
}
}
Session Context
MCP tools maintain session state to avoid requiring IDs on every call.
Session State
// packages/{domain}/mcp/src/session.ts
export interface SessionState {
currentProjectId?: string;
currentStructureId?: string; // serial
currentCheckpointId?: string; // techbook
}
export class Session {
private state: SessionState = {};
get currentProjectId(): string | undefined {
return this.state.currentProjectId;
}
setProject(id: string): void {
this.state.currentProjectId = id;
// Clear dependent state
this.state.currentStructureId = undefined;
this.state.currentCheckpointId = undefined;
}
setStructure(id: string): void {
this.state.currentStructureId = id;
}
clear(): void {
this.state = {};
}
}
Using Session in Tools
// Tool definitions include optional session-filled params
{
name: 'spine_content_get',
inputSchema: {
properties: {
structureId: {
type: 'string',
description: 'Structure ID (uses current structure if not specified)',
},
},
required: [], // structureId can come from session
},
}
// Handler uses session fallback
async function handleContentGet(args, client, session) {
const structureId = args.structureId ?? session.currentStructureId;
if (!structureId) {
return error('No structure selected. Use spine_structure_select first.');
}
// ...
}
Session-Setting Tools
// spine_project_load sets session
async function handleProjectLoad(args, client, session) {
const result = await client.call('project.load', { projectId: args.projectId });
session.setProject(args.projectId);
return success(`Loaded project: ${result.name}`);
}
// spine_structure_select sets session
async function handleStructureSelect(args, client, session) {
const result = await client.call('serial.structure.get', { id: args.structureId });
session.setStructure(args.structureId);
return success(`Selected: ${result.title}`);
}
Response Formatting
Success Responses
function formatCharacterResult(character: Character): string {
return [
`Created character: ${character.name}`,
`ID: ${character.id}`,
`Role: ${character.role}`,
character.traits.length ? `Traits: ${character.traits.join(', ')}` : '',
].filter(Boolean).join('\n');
}
function formatListResult(items: any[], formatItem: (item: any) => string): string {
if (items.length === 0) {
return 'No items found.';
}
return items.map(formatItem).join('\n\n');
}
function formatTreeResult(tree: StructureNode, indent = 0): string {
const prefix = ' '.repeat(indent);
const lines = [`${prefix}${tree.type}: ${tree.title} (${tree.id})`];
for (const child of tree.children ?? []) {
lines.push(formatTreeResult(child, indent + 1));
}
return lines.join('\n');
}
Error Responses
function error(message: string, details?: Record<string, unknown>): CallToolResult {
let text = `Error: ${message}`;
if (details) {
text += '\n' + JSON.stringify(details, null, 2);
}
return {
content: [{ type: 'text', text }],
isError: true,
};
}
// Common error patterns
function noProjectError(): CallToolResult {
return error('No project loaded. Use spine_project_load first.');
}
function notFoundError(type: string, id: string): CallToolResult {
return error(`${type} not found: ${id}`);
}
function confirmationRequired(action: string): CallToolResult {
return error(
`${action} requires confirmation. Set confirm: true to proceed.`,
{ requiresConfirmation: true }
);
}
Deletion Patterns
Destructive operations require explicit confirmation:
{
name: 'spine_project_delete',
description: 'Delete a project (requires confirmation)',
inputSchema: {
properties: {
projectId: { type: 'string' },
confirm: {
type: 'boolean',
description: 'Set to true to confirm deletion',
},
},
required: ['projectId'],
},
}
async function handleProjectDelete(args, client, session) {
if (!args.confirm) {
return confirmationRequired('Project deletion');
}
await client.call('project.delete', { projectId: args.projectId });
// Clear session if deleting current project
if (session.currentProjectId === args.projectId) {
session.clear();
}
return success(`Deleted project: ${args.projectId}`);
}
Server Setup
Entry Point
// packages/serial/mcp/src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { loadConfig } from './config.js';
import { SpineClient } from './client.js';
import { Session } from './session.js';
import { createAllTools, handleToolCall } from './tools/index.js';
async function main() {
const config = loadConfig();
const client = new SpineClient(config.serverUrl);
const session = new Session();
await client.connect();
const server = new Server(
{ name: 'spine-serial', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// Register tools
const tools = createAllTools(client);
server.setRequestHandler('tools/list', async () => ({ tools }));
// Handle tool calls
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
return handleToolCall(name, args ?? {}, client, session);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Configuration
// packages/serial/mcp/src/config.ts
import fs from 'fs';
import path from 'path';
export interface MCPConfig {
serverUrl: string;
autoReconnect: boolean;
reconnectDelay: number;
requestTimeout: number;
}
export function loadConfig(): MCPConfig {
const defaults: MCPConfig = {
serverUrl: 'ws://localhost:8080',
autoReconnect: true,
reconnectDelay: 1000,
requestTimeout: 30000,
};
// Environment variables override
if (process.env.SPINE_SERVER_URL) {
defaults.serverUrl = process.env.SPINE_SERVER_URL;
}
// Config file override
const configPath = path.join(
process.env.HOME ?? '',
'.config/spine/mcp.json'
);
if (fs.existsSync(configPath)) {
const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
Object.assign(defaults, fileConfig);
}
return defaults;
}
Testing Tools
Unit Tests
// packages/serial/mcp/src/tools/__tests__/bible.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { handleCharacterCreate } from '../bible.js';
import { createMockClient, createMockSession } from '../../testing.js';
describe('spine_bible_character_create', () => {
let client: ReturnType<typeof createMockClient>;
let session: ReturnType<typeof createMockSession>;
beforeEach(() => {
client = createMockClient();
session = createMockSession({ currentProjectId: 'test-project' });
});
it('creates a character', async () => {
client.call.mockResolvedValue({
id: 'char-123',
name: 'Elena',
role: 'protagonist',
});
const result = await handleCharacterCreate(
{ name: 'Elena', role: 'protagonist' },
client,
session
);
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain('Elena');
expect(client.call).toHaveBeenCalledWith('serial.bible.character.create', {
projectId: 'test-project',
name: 'Elena',
role: 'protagonist',
traits: [],
description: '',
});
});
it('requires project to be loaded', async () => {
session.currentProjectId = undefined;
const result = await handleCharacterCreate(
{ name: 'Elena', role: 'protagonist' },
client,
session
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('No project loaded');
});
});
Integration Tests
// packages/serial/mcp/src/__tests__/integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn } from 'child_process';
describe('MCP Server Integration', () => {
let serverProcess: ReturnType<typeof spawn>;
beforeAll(async () => {
// Start test server
serverProcess = spawn('node', ['dist/index.js'], {
env: { ...process.env, SPINE_SERVER_URL: 'ws://localhost:8081' },
});
await waitForServer();
});
afterAll(() => {
serverProcess.kill();
});
it('lists tools', async () => {
const response = await sendMCPRequest('tools/list', {});
expect(response.tools).toContainEqual(
expect.objectContaining({ name: 'spine_project_list' })
);
});
});
Claude Desktop Configuration
{
"mcpServers": {
"spine-serial": {
"command": "node",
"args": ["/path/to/spine/packages/serial/mcp/dist/index.js"],
"env": {
"SPINE_SERVER_URL": "ws://localhost:8080"
}
},
"spine-techbook": {
"command": "node",
"args": ["/path/to/spine/packages/techbook/mcp/dist/index.js"],
"env": {
"SPINE_SERVER_URL": "ws://localhost:8081"
}
}
}
}
Checklist for New Tools
- Choose name following
spine_{resource}_{action}pattern - Define input schema with descriptions
- Mark required vs optional parameters
- Handle session context (projectId, structureId, etc.)
- Format success response clearly
- Return proper error responses
- Add confirmation for destructive operations
- Register tool in createAllTools
- Add handler to handleToolCall switch
- Write unit tests
- Document in domain's MCP reference
スコア
総合スコア
50/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です