← スキル一覧に戻る

spine-server-handlers
by cr8or-space
A tool for producing books and stories at scale using LLM assistance.
⭐ 0🍴 0📅 2026年1月8日
SKILL.md
name: spine-server-handlers description: "Guide for adding WebSocket API endpoints to Spine. Covers JSON-RPC 2.0 patterns, domain handler registration, request/response types, error handling, and session context."
Spine Server Handler Development
Overview
Spine uses a WebSocket server with JSON-RPC 2.0 protocol. The framework provides server infrastructure; domains register handlers for their operations.
Architecture
Client (CLI/MCP) → WebSocket → Server → Handler Registry → Domain Handler → Core Logic
- Framework (
packages/framework/server): WebSocket infrastructure, handler registry, session management - Domain (
packages/{serial,techbook}/core): Handler implementations registered at startup
JSON-RPC 2.0 Protocol
Request Format
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"method": "serial.bible.character.create",
"params": {
"projectId": "proj-123",
"name": "Elena",
"role": "protagonist"
}
}
Success Response
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"result": {
"id": "char-456",
"name": "Elena",
"role": "protagonist",
"createdAt": "2025-01-06T00:00:00Z"
}
}
Error Response
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"error": {
"code": -32602,
"message": "Invalid params: name is required",
"data": {
"field": "name",
"reason": "required"
}
}
}
Method Naming Convention
{domain}.{resource}.{subresource?}.{action}
Examples:
serial.bible.character.createserial.bible.character.updateserial.structure.treeserial.generate.starttechbook.concept.createtechbook.tangle.run
Actions:
list- Get all resourcesget- Get single resource by IDcreate- Create new resourceupdate- Update existing resourcedelete- Delete resourcetree- Get hierarchical structurestart/stop/status- For processes
Defining Handlers
Handler Type
// packages/framework/server/types.ts
import { z } from 'zod';
export type Handler<TParams, TResult> = {
params: z.ZodType<TParams>;
result: z.ZodType<TResult>;
handler: (params: TParams, context: HandlerContext) => Promise<TResult>;
};
export type HandlerContext = {
session: SessionState;
services: ServiceContainer;
};
export type SessionState = {
currentProjectId?: string;
currentStructureId?: string;
// Domain can extend with additional state
};
Implementing a Handler
// packages/serial/core/handlers/bible/character.ts
import { z } from 'zod';
import { Handler, HandlerContext } from '@repo/framework/server';
import { CharacterSchema, CreateCharacterSchema } from '@repo/serial/types';
// Define schemas for params and result
const CreateCharacterParamsSchema = CreateCharacterSchema.extend({
projectId: z.string().uuid().optional(), // Optional if session has currentProjectId
});
const CreateCharacterResultSchema = CharacterSchema;
// Implement handler
export const createCharacterHandler: Handler<
z.infer<typeof CreateCharacterParamsSchema>,
z.infer<typeof CreateCharacterResultSchema>
> = {
params: CreateCharacterParamsSchema,
result: CreateCharacterResultSchema,
async handler(params, context) {
// Get project ID from params or session
const projectId = params.projectId ?? context.session.currentProjectId;
if (!projectId) {
throw new RpcError(-32602, 'No project loaded', { field: 'projectId' });
}
// Call core logic
const character = await context.services.bible.createCharacter(projectId, {
name: params.name,
role: params.role,
traits: params.traits ?? [],
});
return character;
},
};
Registering Handlers
// packages/serial/core/handlers/index.ts
import { HandlerRegistry } from '@repo/framework/server';
import { createCharacterHandler, updateCharacterHandler } from './bible/character.js';
import { getStructureTreeHandler } from './structure/tree.js';
export function registerSerialHandlers(registry: HandlerRegistry) {
// Bible handlers
registry.register('serial.bible.character.create', createCharacterHandler);
registry.register('serial.bible.character.update', updateCharacterHandler);
registry.register('serial.bible.character.get', getCharacterHandler);
registry.register('serial.bible.character.list', listCharactersHandler);
registry.register('serial.bible.character.delete', deleteCharacterHandler);
// Structure handlers
registry.register('serial.structure.tree', getStructureTreeHandler);
registry.register('serial.structure.create', createStructureHandler);
// Generation handlers
registry.register('serial.generate.start', startGenerationHandler);
registry.register('serial.generate.status', getGenerationStatusHandler);
// ... more handlers
}
Server Initialization
// apps/serial-server/src/index.ts
import { createServer } from '@repo/framework/server';
import { registerSerialHandlers } from '@repo/serial/core';
const server = createServer({
port: 8080,
dataDir: './data',
});
// Register domain handlers
registerSerialHandlers(server.handlers);
// Start server
await server.listen();
console.log('Serial server listening on ws://localhost:8080');
Error Handling
RPC Error Codes
| Code | Meaning | When to Use |
|---|---|---|
| -32700 | Parse error | Invalid JSON |
| -32600 | Invalid request | Missing required fields |
| -32601 | Method not found | Unknown method |
| -32602 | Invalid params | Validation failed |
| -32603 | Internal error | Unexpected server error |
| -32000 to -32099 | Server error | Application-specific errors |
Application Error Codes
Define domain-specific codes in the -32000 range:
// packages/serial/core/errors.ts
export const SerialErrorCodes = {
PROJECT_NOT_FOUND: -32001,
ENTITY_NOT_FOUND: -32002,
CONTENT_LOCKED: -32003,
GENERATION_IN_PROGRESS: -32004,
CONTINUITY_VIOLATION: -32005,
} as const;
Throwing Errors
import { RpcError } from '@repo/framework/server';
import { SerialErrorCodes } from '../errors.js';
async handler(params, context) {
const character = await context.services.bible.getCharacter(params.id);
if (!character) {
throw new RpcError(
SerialErrorCodes.ENTITY_NOT_FOUND,
`Character not found: ${params.id}`,
{ entityType: 'character', entityId: params.id }
);
}
// Validation errors use -32602
if (!params.name?.trim()) {
throw new RpcError(-32602, 'Name cannot be empty', { field: 'name' });
}
return character;
}
Session Context
Using Session State
The session persists across requests for the same WebSocket connection:
async handler(params, context) {
// Prefer explicit param, fall back to session
const projectId = params.projectId ?? context.session.currentProjectId;
const structureId = params.structureId ?? context.session.currentStructureId;
if (!projectId) {
throw new RpcError(-32602, 'No project loaded. Use project.load first.');
}
// ...
}
Updating Session State
// Handler for project.load
export const loadProjectHandler: Handler<...> = {
async handler(params, context) {
const project = await context.services.project.load(params.projectId);
// Update session state
context.session.currentProjectId = project.id;
context.session.currentStructureId = undefined; // Clear structure selection
return project;
}
};
Common Handler Patterns
List with Filtering
const ListCharactersParamsSchema = z.object({
projectId: z.string().uuid().optional(),
role: z.enum(['protagonist', 'antagonist', 'supporting']).optional(),
limit: z.number().int().positive().max(100).default(50),
offset: z.number().int().nonnegative().default(0),
});
export const listCharactersHandler: Handler<...> = {
params: ListCharactersParamsSchema,
async handler(params, context) {
const projectId = params.projectId ?? context.session.currentProjectId;
if (!projectId) throw new RpcError(-32602, 'No project loaded');
return context.services.bible.listCharacters(projectId, {
role: params.role,
limit: params.limit,
offset: params.offset,
});
}
};
Delete with Confirmation
const DeleteCharacterParamsSchema = z.object({
id: z.string().uuid(),
confirm: z.boolean().default(false),
});
export const deleteCharacterHandler: Handler<...> = {
params: DeleteCharacterParamsSchema,
async handler(params, context) {
if (!params.confirm) {
throw new RpcError(
-32602,
'Deletion requires confirmation. Set confirm: true to proceed.',
{ requiresConfirmation: true }
);
}
await context.services.bible.deleteCharacter(params.id);
return { deleted: true, id: params.id };
}
};
Long-Running Operations
// Start operation - returns immediately with operation ID
export const startGenerationHandler: Handler<...> = {
async handler(params, context) {
const operationId = await context.services.generation.start(params);
return { operationId, status: 'started' };
}
};
// Check status
export const getGenerationStatusHandler: Handler<...> = {
async handler(params, context) {
return context.services.generation.getStatus(params.operationId);
}
};
// Cancel operation
export const cancelGenerationHandler: Handler<...> = {
async handler(params, context) {
await context.services.generation.cancel(params.operationId);
return { cancelled: true };
}
};
Testing Handlers
// packages/serial/core/handlers/__tests__/character.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createCharacterHandler } from '../bible/character.js';
import { createMockContext } from '@repo/framework/server/testing';
describe('createCharacterHandler', () => {
let context: ReturnType<typeof createMockContext>;
beforeEach(() => {
context = createMockContext({
session: { currentProjectId: 'test-project' },
});
});
it('creates a character', async () => {
const result = await createCharacterHandler.handler(
{ name: 'Elena', role: 'protagonist' },
context
);
expect(result.name).toBe('Elena');
expect(result.role).toBe('protagonist');
expect(result.id).toBeDefined();
});
it('throws when no project loaded', async () => {
context.session.currentProjectId = undefined;
await expect(
createCharacterHandler.handler({ name: 'Elena', role: 'protagonist' }, context)
).rejects.toThrow('No project loaded');
});
it('validates params', async () => {
const result = createCharacterHandler.params.safeParse({ role: 'protagonist' });
expect(result.success).toBe(false);
});
});
Checklist for New Handlers
- Define params schema with Zod
- Define result schema with Zod
- Implement handler function
- Handle session context (projectId, structureId)
- Throw appropriate RpcError on failures
- Register handler with correct method name
- Add tests
- Document in API 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
レビュー
💬
レビュー機能は近日公開予定です