MCP Tool Development and Anti-Drift Maintenance
MCP tools are the primary interface between agents and the Myco intelligence pipeline. Each tool requires a coordinated schema ↔ handler ↔ documentation triad that can drift over time, causing silent agent failures. This skill covers the complete development lifecycle and maintenance procedures to prevent drift regressions.
Prerequisites
- Working Myco development environment with
packages/myco/src/tools/ structure
- Understanding of JSON Schema for parameter definitions
- Familiarity with TypeScript handler patterns and DaemonClient usage
- Knowledge of local vs cloud MCP bifurcation model
- Understanding of shared tool-runtime supporting multiple transports (MCP stdio, HTTP MCP, CLI)
Procedure A: Schema Definition
Define the tool interface in packages/myco/src/tools/definitions.ts (shared tool-runtime definitions):
Add tool name constant at the top of the file:
export const TOOL_MY_NEW_TOOL = 'myco_my_new_tool';
Add schema entry to the appropriate array (TOOL_DEFINITIONS for local tools, COLLECTIVE_TOOL_DEFINITIONS for Collective-dependent tools):
{
name: TOOL_MY_NEW_TOOL,
description: 'Brief description of what this tool does — agents use this for selection decisions',
cortex: {
guidance: 'Clear guidance for when to use this tool vs alternatives',
priority: 50, // Default 100, lower numbers = higher priority
requiresTeam: false, // Set true if requires team sync
requiresCollective: false, // Set true if requires Collective connection
},
annotations: {
readOnlyHint: true, // False if tool mutates state
destructiveHint: false, // True if tool can destroy data
idempotentHint: true, // False if repeated calls cause different effects
openWorldHint: false, // True if tool reaches outside local vault
},
inputSchema: {
type: 'object' as const,
properties: {
param_name: {
type: 'string',
description: 'Clear description for agents — include format examples'
},
optional_param: {
type: 'number',
description: 'Optional parameter with default behavior explained'
}
},
required: ['param_name']
}
}
Use descriptive parameter names — agents rely on semantic meaning. session_id is better than id, batch_limit is better than limit.
Document every parameter thoroughly — the description is what agents use to understand usage. Include examples for complex formats.
Set annotations correctly — readOnlyHint: true for read-only tools, destructiveHint: true for tools that can destroy data.
Configure cortex metadata — set requiresCollective: true for tools that only work when connected to a Collective.
Avoid OpenAI strict mode incompatibilities — OpenAI's strict JSON Schema mode rejects oneOf, anyOf, allOf, enum, and not keywords at the top level. Use simple types with clear descriptions instead:
// BAD: OpenAI strict mode rejects this
{
param_status: {
enum: ['active', 'inactive', 'pending'],
description: 'Status value'
}
}
// GOOD: Use string with enum values in description
{
param_status: {
type: 'string',
description: 'Status value. Must be one of: active, inactive, pending'
}
}
Avoid Zod refinement-like patterns — Schema constructs that imply Zod refinements cause silent tool registration failures across runtimes. Never use .default(), .min(), .max(), or .refine() patterns in schema definitions:
// BAD: Implies Zod refinements that cause registration failures
{
limit: {
type: 'number',
default: 10, // Zod .default() - causes registration failure
minimum: 1, // Zod .min() - causes registration failure
maximum: 100 // Zod .max() - causes registration failure
}
}
// GOOD: Use plain schema with behavior described in documentation
{
limit: {
type: 'number',
description: 'Maximum items to return (1-100, defaults to 10 if omitted)'
}
}
Procedure B: Handler Implementation
Create the handler in packages/myco/src/tools/my-new-tool.ts:
Import required types and client:
import type { DaemonClient } from '@myco/hooks/client.js';
import { buildEndpoint } from './shared.js';
import { ToolFailure } from './error.js';
Define input and result types:
interface MyNewToolInput {
param_name: string;
optional_param?: number;
}
interface MyNewToolResult {
id: string;
status: string;
// ... other result fields
}
Implement the handler function with canonical error handling:
export async function handleMyNewTool(
input: MyNewToolInput,
client: DaemonClient,
): Promise<MyNewToolResult> {
// Validate all schema-declared parameters are consumed
const { param_name, optional_param = defaultValue } = input;
try {
const endpoint = buildEndpoint('/api/some-operation', {
param_name,
optional_param,
});
const response = await client.get(endpoint);
return response as MyNewToolResult;
} catch (error) {
// Use ToolFailure for consistent agent-facing error structure
throw new ToolFailure(`Failed to execute ${TOOL_MY_NEW_TOOL}`, {
cause: error,
context: { param_name, optional_param },
retriable: true, // Set false for permanent failures
});
}
}
Consume every schema parameter — handlers must accept exactly what the schema advertises. Renamed params, dropped fields, or undocumented required args cause agent call failures.
Use DaemonClient for vault access — all tools proxy through the daemon HTTP API via client.get(), client.post(), etc.
Handle errors with ToolFailure interface — import from packages/myco/src/tools/error.ts and wrap operational failures with structured context agents can understand:
import { ToolFailure } from './error.js';
// For parameter validation errors
throw new ToolFailure('Invalid parameter format', {
context: { param_name, expected: 'uuid format' },
retriable: false,
});
// For network/service failures
throw new ToolFailure('Daemon API unavailable', {
cause: originalError,
context: { endpoint, attempt: retryCount },
retriable: true,
});
Use canonical ToolFailure interface pattern — packages/myco/src/tools/error.ts exports the standardized ToolFailure class with structured error context. Always import and use this interface for agent-facing errors:
import { ToolFailure, ToolErrorCode } from './error.js';
// Standard validation failure
throw new ToolFailure('Parameter validation failed', {
code: ToolErrorCode.INVALID_INPUT,
context: { param: value, constraint: 'must be uuid' },
retriable: false,
});
// Service availability failure
throw new ToolFailure('Daemon service unavailable', {
code: ToolErrorCode.SERVICE_UNAVAILABLE,
cause: networkError,
context: { endpoint, timeout_ms: 5000 },
retriable: true,
});
Procedure C: Multi-Transport Registration
Register the tool once in the shared tool runtime. Stdio MCP, HTTP MCP, and the CLI all call through packages/myco/src/tools/index.ts; do not add transport-specific switch cases for normal tools.
Shared runtime registration in packages/myco/src/tools/index.ts:
[TOOL_MY_NEW_TOOL, async () => {
const { handleMyNewTool } = await import('./my-new-tool.js');
return {
handle: (input, client) => handleMyNewTool(input as MyNewToolInput, client),
summarize: (input, result) => ({ param_name: input.param_name }),
};
}],
MCP stdio and HTTP registration — packages/myco/src/mcp/server.ts exposes the shared definitions from packages/myco/src/tools/definitions.ts and dispatches calls through createMycoTools(...).
CLI registration — myco tool list and myco tool call use the same shared runtime, so the tool becomes available there after it is in TOOL_DEFINITIONS and HANDLERS.
Test multi-transport availability — verify tool appears in MCP client tool list, HTTP MCP endpoints, and CLI help for all configured transports.
Configure conditional enablement — Collective tools are automatically enabled/disabled based on collectiveEnabled flag across all transports. Local tools are always available.
Procedure D: Documentation Bundling and Regeneration
Each tool carries inline SKILL.md documentation bundled at compile time across all transports:
Write clear tool documentation covering:
- When to use this tool vs alternatives
- Parameter meanings and examples
- Expected response format
- Common usage patterns
- Transport-specific considerations (MCP stdio vs HTTP vs CLI)
Bundle at build time — documentation is compiled into handlers during build process and shared across transports.
Regenerate after schema changes:
npm run build # Rebuild bundled documentation for all transports
Verify agent-visible docs — test that agents receive current parameter names and descriptions across all transport types, not stale snapshots.
Never ship handler changes without doc updates — mismatched documentation causes agents to call tools with wrong parameters across any transport.
Procedure E: Anti-Drift Testing Patterns
Implement systematic checks to catch schema-handler-documentation drift across the shared tool-runtime:
Create test file template (example: packages/myco/src/tools/definitions.test.ts):
import { describe, test, expect } from 'vitest';
import { TOOL_DEFINITIONS, COLLECTIVE_TOOL_DEFINITIONS } from './definitions.js';
import * as handlers from './index.js';
Schema-handler parameter alignment test:
test('all schema parameters referenced in handler source', () => {
const allTools = [...TOOL_DEFINITIONS, ...COLLECTIVE_TOOL_DEFINITIONS];
for (const tool of allTools) {
const handlerName = getHandlerNameForTool(tool.name);
const handler = handlers[handlerName];
if (!handler) continue; // Skip stubs
const schemaParams = Object.keys(tool.inputSchema.properties || {});
const handlerSource = handler.toString();
schemaParams.forEach(param => {
expect(handlerSource).toContain(param);
});
}
});
Handler-schema synchronization test:
test('no orphaned handler parameters', () => {
// Parse handler destructuring patterns, compare to schema
// Fail if handler expects parameters not in schema
});
Tool name constant consistency:
test('all tool names match exported constants', () => {
const allTools = [...TOOL_DEFINITIONS, ...COLLECTIVE_TOOL_DEFINITIONS];
allTools.forEach(tool => {
expect(tool.name).toMatch(/^(myco_|collective_)/);
// Verify constant exists and matches
});
});
Multi-transport registration consistency:
test('all tools registered across transports', () => {
// Verify MCP stdio, HTTP MCP, and CLI registrations are consistent
// Check that transport-specific configurations align with shared definitions
});
Run after every handler or schema change — drift failures are silent until agents encounter them in production across any transport.
Procedure F: Stub vs Documented Tool Discipline
Handle incomplete or placeholder tools appropriately:
Mark stubs explicitly in schema description:
{
name: TOOL_MY_STUB,
description: '[STUB] This tool is registered but not yet implemented. Returns placeholder response only.',
// ... rest of definition
}
Implement stub handlers that return consistent "not implemented" responses:
export async function handleMyStub(): Promise<{ status: string }> {
return { status: 'not_implemented' };
}
Never document stubs as working tools — agents should know when functionality is incomplete across all transports.
Test stub behavior — ensure stubs return consistent responses rather than errors across MCP stdio, HTTP MCP, and CLI.
Remove or implement — stubs confuse agents across all transports. Either complete the implementation or remove from schema entirely.
Procedure G: Cloud vs Local Placement Decisions
Decide whether new tools belong in local or cloud MCP surface:
Default to local-only — new tools go in TOOL_DEFINITIONS unless they meet cloud criteria.
Promote to cloud surface only if tool is:
- Semantically read-only (no vault writes)
- Safe for federation (no sensitive data exposure)
- Required for cross-project Collective operations
Use COLLECTIVE_TOOL_DEFINITIONS for tools that require Collective connection state.
Test both surfaces — verify tools work correctly in local MCP and (if applicable) cloud federation.
Document placement rationale — explain why tool belongs in its chosen surface.
Procedure H: Skill Lifecycle Tool Registration Patterns
The skill lifecycle system requires specific tools that follow domain-specific registration patterns:
Register skill candidate management tools:
// In packages/myco/src/tools/definitions.ts
export const TOOL_SKILL_CANDIDATES = 'myco_skill_candidates';
{
name: TOOL_SKILL_CANDIDATES,
description: 'Manage skill candidates (identified topics that may become skills). Supports list, get, create, and update actions.',
cortex: {
guidance: 'Use for candidate discovery, approval workflows, and candidate lifecycle management',
priority: 80,
requiresTeam: false,
requiresCollective: false,
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Action to perform: list, get, create, update, delete'
},
id: {
type: 'string',
description: 'Candidate ID (required for get/update)'
},
topic: {
type: 'string',
description: 'Skill topic (required for create)'
},
rationale: {
type: 'string',
description: 'Why this should be a skill (required for create)'
},
status: {
type: 'string',
description: 'Candidate status: identified, dismissed'
}
},
required: ['action']
}
}
Register skill record management tools:
export const TOOL_SKILL_RECORDS = 'myco_skill_records';
{
name: TOOL_SKILL_RECORDS,
description: 'Read, update, and delete skill records (materialized skills on disk). Supports list, get, update, and delete actions.',
cortex: {
guidance: 'Use for skill lifecycle operations — reading existing skills, updating status, managing skill evolution',
priority: 80,
},
inputSchema: {
type: 'object',
properties: {
action: { type: 'string', description: 'Action: list, get, update, delete' },
id: { type: 'string', description: 'Skill record ID or name (for get/update/delete)' },
status: { type: 'string', description: 'Filter by or new status: active, stale, retired' },
generation: { type: 'number', description: 'New generation number (for update)' }
},
required: ['action']
}
}
Register skill file writing tools:
export const TOOL_WRITE_SKILL = 'myco_write_skill';
{
name: TOOL_WRITE_SKILL,
description: 'Write a SKILL.md file to disk and create or update the corresponding skill record and lineage entry.',
cortex: {
guidance: 'Use when materializing a new skill from approved candidates or updating existing skills',
priority: 90,
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Skill directory name (kebab-case, NO colon)' },
display_name: { type: 'string', description: 'Human-readable display name' },
description: { type: 'string', description: 'Short description of what the skill does' },
content: { type: 'string', description: 'Full SKILL.md content in markdown' },
rationale: { type: 'string', description: 'Why this skill was created or updated' },
candidate_id: { type: 'string', description: 'Candidate ID that prompted this skill creation' }
},
required: ['name', 'display_name', 'description', 'content']
}
}
Implement domain-specific validation patterns — skill tools require structural validation (YAML frontmatter, name prefixes, content length limits) that differs from general vault tools.
Test skill workflow integration — verify tools support the complete skill lifecycle: survey → approve → generate → evolve.
Procedure I: Shared Tool-Runtime Integration
Integrate with the shared tool-runtime supporting multiple transports:
Configure transport-specific behaviors:
// Different transports may need different error handling
const formatResponse = (result: any, transport: 'mcp' | 'http' | 'cli') => {
switch (transport) {
case 'mcp': return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
case 'http': return result; // Direct JSON response
case 'cli': return formatCliOutput(result); // Human-readable format
}
};
Handle transport-specific authentication — MCP stdio uses connection-level auth, HTTP MCP uses token auth, CLI uses file-based auth.
Implement transport-aware logging:
logActivity(TOOL_NAME, {
...params,
transport: 'mcp|http|cli',
duration_ms: Date.now() - start
});
Test cross-transport consistency — verify same input produces equivalent results across MCP stdio, HTTP MCP, and CLI transports.
Document transport differences — note any transport-specific behaviors or limitations in tool documentation.
Procedure J: Grove Migration Context Handling
Handle project context changes with Grove migration architecture:
Update context injection patterns for Grove migration compatibility:
// Old: Direct project context injection
const context = await injectProjectContext(sessionId);
// New: Grove-aware context injection with fallback
const context = await injectGroveContext(sessionId, {
fallbackToLocal: true,
respectGroveConfig: true,
});
Handle Grove project boundaries when tools access cross-project resources:
export async function handleCrossProjectTool(
input: CrossProjectInput,
client: DaemonClient,
): Promise<CrossProjectResult> {
// Validate Grove project permissions before access
const groveAccess = await client.get('/api/grove/validate-access', {
targetProject: input.project_id,
operation: 'read',
});
if (!groveAccess.allowed) {
throw new ToolFailure('Grove access denied', {
context: { project_id: input.project_id, reason: groveAccess.reason },
retriable: false,
});
}
// Proceed with Grove-scoped operation
return await performGroveOperation(input, client);
}
Configure Grove-aware tool definitions with proper scope annotations:
{
name: TOOL_GROVE_AWARE,
description: 'Tool that operates across Grove project boundaries',
cortex: {
guidance: 'Use for cross-project operations in Grove environments',
requiresGrove: true, // New Grove requirement flag
},
annotations: {
crossProjectHint: true, // Indicates Grove boundary crossing
},
}
Test Grove project isolation — verify tools respect Grove project boundaries and fail gracefully when Grove is not configured.
Document Grove migration impact — note how tools behave differently in Grove vs traditional project structures.
Cross-Cutting Gotchas
Silent parameter drops: When schema defines a parameter but handler ignores it, agents receive no error — their input is silently dropped. This is the most common drift failure.
Documentation lag: Bundled SKILL.md becomes stale when handlers change. Always regenerate documentation after schema or handler modifications.
Cloud surface leakage: Write operations must never leak to cloud MCP surface. Default to local-only; promote to cloud only with explicit read-only verification.
Validation vs runtime divergence: Schema validation passes but handler expects different parameter structure. Test actual invocations, not just schema validation.
Collective conditional enablement: collective_* tools are enabled by Collective connection state. Test both connected and disconnected scenarios across all transports.
Tool name consistency: Use myco_ prefix for standard tools, collective_ prefix for Collective-dependent tools. Avoid generic names that conflict with other MCP servers.
Handler signature mismatch: All handlers must accept (input, client) parameters. Missing DaemonClient parameter causes registration failures.
Cross-runtime schema compatibility: OpenAI strict mode and Zod refinement patterns cause silent registration failures. Use plain JSON Schema types with descriptive documentation instead of complex validation constructs.
Skill tool registration gaps: Skill lifecycle operations require complete tool registration (candidates, records, write_skill) — missing any component breaks agent workflows. Always register skill tools as a complete set.
Shared tool-runtime path shifts: Tool definitions live in packages/myco/src/tools/definitions.ts. Update import paths and test references when the shared runtime moves.
Multi-transport registration complexity: Shared tool-runtime requires consistent registration across MCP stdio, HTTP MCP, and CLI transports. Test all transports when adding new tools.
Transport-specific error handling: Different transports expect different response formats. Implement transport-aware error formatting to prevent agent confusion.
ToolFailure anti-pattern: Never throw raw Error objects from handlers — always wrap with ToolFailure interface for consistent agent error handling. Missing structured error context causes agent confusion across all transports.
Code duplication across tool surface: Avoid copy-pasting handler patterns between tools. Extract shared utilities to packages/myco/src/tools/shared.ts and import consistently. Duplicated validation logic, error handling, and response formatting patterns create maintenance burden and drift risks across the unified tool surface. Use composition patterns instead:
// BAD: Duplicated validation across multiple tools
function handleToolA(input) {
if (!input.session_id || !isValidUuid(input.session_id)) {
throw new ToolFailure('Invalid session_id');
}
// ... tool logic
}
function handleToolB(input) {
if (!input.session_id || !isValidUuid(input.session_id)) {
throw new ToolFailure('Invalid session_id');
}
// ... tool logic (nearly identical validation)
}
// GOOD: Shared validation utilities
import { validateSessionId, validateRequiredString } from './shared.js';
function handleToolA(input) {
validateSessionId(input.session_id);
// ... tool logic
}
function handleToolB(input) {
validateSessionId(input.session_id);
// ... tool logic
}
Grove context injection failures: Tools accessing project context must handle Grove migration gracefully. Missing Grove-aware context injection causes failures in Grove environments while working in traditional project structures, creating environment-specific bugs.
1---2name: myco-mcp-tool-development-lifecycle3description: Comprehensive lifecycle for authoring, registering, documenting, and maintaining MCP tools in packages/myco/src/tools/ — covering schema definition in TOOL_DEFINITIONS arrays, handler implementation with DaemonClient patterns, shared tool-runtime registration, documentation bundling, anti-drift testing patterns, and cloud vs local placement decisions. Essential for maintaining the schema ↔ handler ↔ documentation triad that agents depend on for correct tool invocations, even when the user doesn't explicitly ask for MCP tool development.4---5
6# MCP Tool Development and Anti-Drift Maintenance
7
8MCP tools are the primary interface between agents and the Myco intelligence pipeline. Each tool requires a coordinated schema ↔ handler ↔ documentation triad that can drift over time, causing silent agent failures. This skill covers the complete development lifecycle and maintenance procedures to prevent drift regressions.
9
10## Prerequisites
11
12- Working Myco development environment with `packages/myco/src/tools/` structure
13- Understanding of JSON Schema for parameter definitions
14- Familiarity with TypeScript handler patterns and DaemonClient usage
15- Knowledge of local vs cloud MCP bifurcation model
16- Understanding of shared tool-runtime supporting multiple transports (MCP stdio, HTTP MCP, CLI)
17
18## Procedure A: Schema Definition
19
20Define the tool interface in `packages/myco/src/tools/definitions.ts` (shared tool-runtime definitions):
21
221. **Add tool name constant** at the top of the file:
23 ```typescript
24 export const TOOL_MY_NEW_TOOL = 'myco_my_new_tool';
25 ```
26
272. **Add schema entry** to the appropriate array (`TOOL_DEFINITIONS` for local tools, `COLLECTIVE_TOOL_DEFINITIONS` for Collective-dependent tools):
28 ```typescript
29 {
30 name: TOOL_MY_NEW_TOOL,
31 description: 'Brief description of what this tool does — agents use this for selection decisions',
32 cortex: {
33 guidance: 'Clear guidance for when to use this tool vs alternatives',
34 priority: 50, // Default 100, lower numbers = higher priority
35 requiresTeam: false, // Set true if requires team sync
36 requiresCollective: false, // Set true if requires Collective connection
37 },
38 annotations: {
39 readOnlyHint: true, // False if tool mutates state
40 destructiveHint: false, // True if tool can destroy data
41 idempotentHint: true, // False if repeated calls cause different effects
42 openWorldHint: false, // True if tool reaches outside local vault
43 },
44 inputSchema: {
45 type: 'object' as const,
46 properties: {
47 param_name: {
48 type: 'string',
49 description: 'Clear description for agents — include format examples'
50 },
51 optional_param: {
52 type: 'number',
53 description: 'Optional parameter with default behavior explained'
54 }
55 },
56 required: ['param_name']
57 }
58 }
59 ```
60
613. **Use descriptive parameter names** — agents rely on semantic meaning. `session_id` is better than `id`, `batch_limit` is better than `limit`.
62
634. **Document every parameter thoroughly** — the description is what agents use to understand usage. Include examples for complex formats.
64
655. **Set annotations correctly** — `readOnlyHint: true` for read-only tools, `destructiveHint: true` for tools that can destroy data.
66
676. **Configure cortex metadata** — set `requiresCollective: true` for tools that only work when connected to a Collective.
68
697. **Avoid OpenAI strict mode incompatibilities** — OpenAI's strict JSON Schema mode rejects `oneOf`, `anyOf`, `allOf`, `enum`, and `not` keywords at the top level. Use simple types with clear descriptions instead:
70 ```typescript
71 // BAD: OpenAI strict mode rejects this
72 {
73 param_status: {
74 enum: ['active', 'inactive', 'pending'],
75 description: 'Status value'
76 }
77 }
78
79 // GOOD: Use string with enum values in description
80 {
81 param_status: {
82 type: 'string',
83 description: 'Status value. Must be one of: active, inactive, pending'
84 }
85 }
86 ```
87
888. **Avoid Zod refinement-like patterns** — Schema constructs that imply Zod refinements cause silent tool registration failures across runtimes. Never use `.default()`, `.min()`, `.max()`, or `.refine()` patterns in schema definitions:
89 ```typescript
90 // BAD: Implies Zod refinements that cause registration failures
91 {
92 limit: {
93 type: 'number',
94 default: 10, // Zod .default() - causes registration failure
95 minimum: 1, // Zod .min() - causes registration failure
96 maximum: 100 // Zod .max() - causes registration failure
97 }
98 }
99
100 // GOOD: Use plain schema with behavior described in documentation
101 {
102 limit: {
103 type: 'number',
104 description: 'Maximum items to return (1-100, defaults to 10 if omitted)'
105 }
106 }
107 ```
108
109## Procedure B: Handler Implementation
110
111Create the handler in `packages/myco/src/tools/my-new-tool.ts`:
112
1131. **Import required types and client**:
114 ```typescript
115 import type { DaemonClient } from '@myco/hooks/client.js';
116 import { buildEndpoint } from './shared.js';
117 import { ToolFailure } from './error.js';
118 ```
119
1202. **Define input and result types**:
121 ```typescript
122 interface MyNewToolInput {
123 param_name: string;
124 optional_param?: number;
125 }
126
127 interface MyNewToolResult {
128 id: string;
129 status: string;
130 // ... other result fields
131 }
132 ```
133
1343. **Implement the handler function with canonical error handling**:
135 ```typescript
136 export async function handleMyNewTool(
137 input: MyNewToolInput,
138 client: DaemonClient,
139 ): Promise<MyNewToolResult> {
140 // Validate all schema-declared parameters are consumed
141 const { param_name, optional_param = defaultValue } = input;
142
143 try {
144 const endpoint = buildEndpoint('/api/some-operation', {
145 param_name,
146 optional_param,
147 });
148
149 const response = await client.get(endpoint);
150 return response as MyNewToolResult;
151 } catch (error) {
152 // Use ToolFailure for consistent agent-facing error structure
153 throw new ToolFailure(`Failed to execute ${TOOL_MY_NEW_TOOL}`, {
154 cause: error,
155 context: { param_name, optional_param },
156 retriable: true, // Set false for permanent failures
157 });
158 }
159 }
160 ```
161
1624. **Consume every schema parameter** — handlers must accept exactly what the schema advertises. Renamed params, dropped fields, or undocumented required args cause agent call failures.
163
1645. **Use DaemonClient for vault access** — all tools proxy through the daemon HTTP API via `client.get()`, `client.post()`, etc.
165
1666. **Handle errors with ToolFailure interface** — import from `packages/myco/src/tools/error.ts` and wrap operational failures with structured context agents can understand:
167 ```typescript
168 import { ToolFailure } from './error.js';
169
170 // For parameter validation errors
171 throw new ToolFailure('Invalid parameter format', {
172 context: { param_name, expected: 'uuid format' },
173 retriable: false,
174 });
175
176 // For network/service failures
177 throw new ToolFailure('Daemon API unavailable', {
178 cause: originalError,
179 context: { endpoint, attempt: retryCount },
180 retriable: true,
181 });
182 ```
183
1847. **Use canonical ToolFailure interface pattern** — `packages/myco/src/tools/error.ts` exports the standardized `ToolFailure` class with structured error context. Always import and use this interface for agent-facing errors:
185 ```typescript
186 import { ToolFailure, ToolErrorCode } from './error.js';
187
188 // Standard validation failure
189 throw new ToolFailure('Parameter validation failed', {
190 code: ToolErrorCode.INVALID_INPUT,
191 context: { param: value, constraint: 'must be uuid' },
192 retriable: false,
193 });
194
195 // Service availability failure
196 throw new ToolFailure('Daemon service unavailable', {
197 code: ToolErrorCode.SERVICE_UNAVAILABLE,
198 cause: networkError,
199 context: { endpoint, timeout_ms: 5000 },
200 retriable: true,
201 });
202 ```
203
204## Procedure C: Multi-Transport Registration
205
206Register the tool once in the shared tool runtime. Stdio MCP, HTTP MCP, and the CLI all call through `packages/myco/src/tools/index.ts`; do not add transport-specific switch cases for normal tools.
207
2081. **Shared runtime registration** in `packages/myco/src/tools/index.ts`:
209 ```typescript
210 [TOOL_MY_NEW_TOOL, async () => {
211 const { handleMyNewTool } = await import('./my-new-tool.js');
212 return {
213 handle: (input, client) => handleMyNewTool(input as MyNewToolInput, client),
214 summarize: (input, result) => ({ param_name: input.param_name }),
215 };
216 }],
217 ```
218
2192. **MCP stdio and HTTP registration** — `packages/myco/src/mcp/server.ts` exposes the shared definitions from `packages/myco/src/tools/definitions.ts` and dispatches calls through `createMycoTools(...)`.
220
2213. **CLI registration** — `myco tool list` and `myco tool call` use the same shared runtime, so the tool becomes available there after it is in `TOOL_DEFINITIONS` and `HANDLERS`.
222
2234. **Test multi-transport availability** — verify tool appears in MCP client tool list, HTTP MCP endpoints, and CLI help for all configured transports.
224
2255. **Configure conditional enablement** — Collective tools are automatically enabled/disabled based on `collectiveEnabled` flag across all transports. Local tools are always available.
226
227## Procedure D: Documentation Bundling and Regeneration
228
229Each tool carries inline SKILL.md documentation bundled at compile time across all transports:
230
2311. **Write clear tool documentation** covering:
232 - When to use this tool vs alternatives
233 - Parameter meanings and examples
234 - Expected response format
235 - Common usage patterns
236 - Transport-specific considerations (MCP stdio vs HTTP vs CLI)
237
2382. **Bundle at build time** — documentation is compiled into handlers during build process and shared across transports.
239
2403. **Regenerate after schema changes**:
241 ```bash
242 npm run build # Rebuild bundled documentation for all transports
243 ```
244
2454. **Verify agent-visible docs** — test that agents receive current parameter names and descriptions across all transport types, not stale snapshots.
246
2475. **Never ship handler changes without doc updates** — mismatched documentation causes agents to call tools with wrong parameters across any transport.
248
249## Procedure E: Anti-Drift Testing Patterns
250
251Implement systematic checks to catch schema-handler-documentation drift across the shared tool-runtime:
252
2531. **Create test file template** (example: `packages/myco/src/tools/definitions.test.ts`):
254 ```typescript
255 import { describe, test, expect } from 'vitest';
256 import { TOOL_DEFINITIONS, COLLECTIVE_TOOL_DEFINITIONS } from './definitions.js';
257 import * as handlers from './index.js';
258 ```
259
2602. **Schema-handler parameter alignment test**:
261 ```typescript
262 test('all schema parameters referenced in handler source', () => {
263 const allTools = [...TOOL_DEFINITIONS, ...COLLECTIVE_TOOL_DEFINITIONS];
264
265 for (const tool of allTools) {
266 const handlerName = getHandlerNameForTool(tool.name);
267 const handler = handlers[handlerName];
268 if (!handler) continue; // Skip stubs
269
270 const schemaParams = Object.keys(tool.inputSchema.properties || {});
271 const handlerSource = handler.toString();
272
273 schemaParams.forEach(param => {
274 expect(handlerSource).toContain(param);
275 });
276 }
277 });
278 ```
279
2803. **Handler-schema synchronization test**:
281 ```typescript
282 test('no orphaned handler parameters', () => {
283 // Parse handler destructuring patterns, compare to schema
284 // Fail if handler expects parameters not in schema
285 });
286 ```
287
2884. **Tool name constant consistency**:
289 ```typescript
290 test('all tool names match exported constants', () => {
291 const allTools = [...TOOL_DEFINITIONS, ...COLLECTIVE_TOOL_DEFINITIONS];
292
293 allTools.forEach(tool => {
294 expect(tool.name).toMatch(/^(myco_|collective_)/);
295 // Verify constant exists and matches
296 });
297 });
298 ```
299
3005. **Multi-transport registration consistency**:
301 ```typescript
302 test('all tools registered across transports', () => {
303 // Verify MCP stdio, HTTP MCP, and CLI registrations are consistent
304 // Check that transport-specific configurations align with shared definitions
305 });
306 ```
307
3086. **Run after every handler or schema change** — drift failures are silent until agents encounter them in production across any transport.
309
310## Procedure F: Stub vs Documented Tool Discipline
311
312Handle incomplete or placeholder tools appropriately:
313
3141. **Mark stubs explicitly** in schema description:
315 ```typescript
316 {
317 name: TOOL_MY_STUB,
318 description: '[STUB] This tool is registered but not yet implemented. Returns placeholder response only.',
319 // ... rest of definition
320 }
321 ```
322
3232. **Implement stub handlers** that return consistent "not implemented" responses:
324 ```typescript
325 export async function handleMyStub(): Promise<{ status: string }> {
326 return { status: 'not_implemented' };
327 }
328 ```
329
3303. **Never document stubs as working tools** — agents should know when functionality is incomplete across all transports.
331
3324. **Test stub behavior** — ensure stubs return consistent responses rather than errors across MCP stdio, HTTP MCP, and CLI.
333
3345. **Remove or implement** — stubs confuse agents across all transports. Either complete the implementation or remove from schema entirely.
335
336## Procedure G: Cloud vs Local Placement Decisions
337
338Decide whether new tools belong in local or cloud MCP surface:
339
3401. **Default to local-only** — new tools go in `TOOL_DEFINITIONS` unless they meet cloud criteria.
341
3422. **Promote to cloud surface** only if tool is:
343 - Semantically read-only (no vault writes)
344 - Safe for federation (no sensitive data exposure)
345 - Required for cross-project Collective operations
346
3473. **Use `COLLECTIVE_TOOL_DEFINITIONS`** for tools that require Collective connection state.
348
3494. **Test both surfaces** — verify tools work correctly in local MCP and (if applicable) cloud federation.
350
3515. **Document placement rationale** — explain why tool belongs in its chosen surface.
352
353## Procedure H: Skill Lifecycle Tool Registration Patterns
354
355The skill lifecycle system requires specific tools that follow domain-specific registration patterns:
356
3571. **Register skill candidate management tools**:
358 ```typescript
359 // In packages/myco/src/tools/definitions.ts
360 export const TOOL_SKILL_CANDIDATES = 'myco_skill_candidates';
361
362 {
363 name: TOOL_SKILL_CANDIDATES,
364 description: 'Manage skill candidates (identified topics that may become skills). Supports list, get, create, and update actions.',
365 cortex: {
366 guidance: 'Use for candidate discovery, approval workflows, and candidate lifecycle management',
367 priority: 80,
368 requiresTeam: false,
369 requiresCollective: false,
370 },
371 annotations: {
372 readOnlyHint: false,
373 destructiveHint: false,
374 idempotentHint: false,
375 openWorldHint: false,
376 },
377 inputSchema: {
378 type: 'object',
379 properties: {
380 action: {
381 type: 'string',
382 description: 'Action to perform: list, get, create, update, delete'
383 },
384 id: {
385 type: 'string',
386 description: 'Candidate ID (required for get/update)'
387 },
388 topic: {
389 type: 'string',
390 description: 'Skill topic (required for create)'
391 },
392 rationale: {
393 type: 'string',
394 description: 'Why this should be a skill (required for create)'
395 },
396 status: {
397 type: 'string',
398 description: 'Candidate status: identified, dismissed'
399 }
400 },
401 required: ['action']
402 }
403 }
404 ```
405
4062. **Register skill record management tools**:
407 ```typescript
408 export const TOOL_SKILL_RECORDS = 'myco_skill_records';
409
410 {
411 name: TOOL_SKILL_RECORDS,
412 description: 'Read, update, and delete skill records (materialized skills on disk). Supports list, get, update, and delete actions.',
413 cortex: {
414 guidance: 'Use for skill lifecycle operations — reading existing skills, updating status, managing skill evolution',
415 priority: 80,
416 },
417 inputSchema: {
418 type: 'object',
419 properties: {
420 action: { type: 'string', description: 'Action: list, get, update, delete' },
421 id: { type: 'string', description: 'Skill record ID or name (for get/update/delete)' },
422 status: { type: 'string', description: 'Filter by or new status: active, stale, retired' },
423 generation: { type: 'number', description: 'New generation number (for update)' }
424 },
425 required: ['action']
426 }
427 }
428 ```
429
4303. **Register skill file writing tools**:
431 ```typescript
432 export const TOOL_WRITE_SKILL = 'myco_write_skill';
433
434 {
435 name: TOOL_WRITE_SKILL,
436 description: 'Write a SKILL.md file to disk and create or update the corresponding skill record and lineage entry.',
437 cortex: {
438 guidance: 'Use when materializing a new skill from approved candidates or updating existing skills',
439 priority: 90,
440 },
441 annotations: {
442 readOnlyHint: false,
443 destructiveHint: false,
444 idempotentHint: false,
445 openWorldHint: false,
446 },
447 inputSchema: {
448 type: 'object',
449 properties: {
450 name: { type: 'string', description: 'Skill directory name (kebab-case, NO colon)' },
451 display_name: { type: 'string', description: 'Human-readable display name' },
452 description: { type: 'string', description: 'Short description of what the skill does' },
453 content: { type: 'string', description: 'Full SKILL.md content in markdown' },
454 rationale: { type: 'string', description: 'Why this skill was created or updated' },
455 candidate_id: { type: 'string', description: 'Candidate ID that prompted this skill creation' }
456 },
457 required: ['name', 'display_name', 'description', 'content']
458 }
459 }
460 ```
461
4624. **Implement domain-specific validation patterns** — skill tools require structural validation (YAML frontmatter, name prefixes, content length limits) that differs from general vault tools.
463
4645. **Test skill workflow integration** — verify tools support the complete skill lifecycle: survey → approve → generate → evolve.
465
466## Procedure I: Shared Tool-Runtime Integration
467
468Integrate with the shared tool-runtime supporting multiple transports:
469
4701. **Configure transport-specific behaviors**:
471 ```typescript
472 // Different transports may need different error handling
473 const formatResponse = (result: any, transport: 'mcp' | 'http' | 'cli') => {
474 switch (transport) {
475 case 'mcp': return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
476 case 'http': return result; // Direct JSON response
477 case 'cli': return formatCliOutput(result); // Human-readable format
478 }
479 };
480 ```
481
4822. **Handle transport-specific authentication** — MCP stdio uses connection-level auth, HTTP MCP uses token auth, CLI uses file-based auth.
483
4843. **Implement transport-aware logging**:
485 ```typescript
486 logActivity(TOOL_NAME, {
487 ...params,
488 transport: 'mcp|http|cli',
489 duration_ms: Date.now() - start
490 });
491 ```
492
4934. **Test cross-transport consistency** — verify same input produces equivalent results across MCP stdio, HTTP MCP, and CLI transports.
494
4955. **Document transport differences** — note any transport-specific behaviors or limitations in tool documentation.
496
497## Procedure J: Grove Migration Context Handling
498
499Handle project context changes with Grove migration architecture:
500
5011. **Update context injection patterns** for Grove migration compatibility:
502 ```typescript
503 // Old: Direct project context injection
504 const context = await injectProjectContext(sessionId);
505
506 // New: Grove-aware context injection with fallback
507 const context = await injectGroveContext(sessionId, {
508 fallbackToLocal: true,
509 respectGroveConfig: true,
510 });
511 ```
512
5132. **Handle Grove project boundaries** when tools access cross-project resources:
514 ```typescript
515 export async function handleCrossProjectTool(
516 input: CrossProjectInput,
517 client: DaemonClient,
518 ): Promise<CrossProjectResult> {
519 // Validate Grove project permissions before access
520 const groveAccess = await client.get('/api/grove/validate-access', {
521 targetProject: input.project_id,
522 operation: 'read',
523 });
524
525 if (!groveAccess.allowed) {
526 throw new ToolFailure('Grove access denied', {
527 context: { project_id: input.project_id, reason: groveAccess.reason },
528 retriable: false,
529 });
530 }
531
532 // Proceed with Grove-scoped operation
533 return await performGroveOperation(input, client);
534 }
535 ```
536
5373. **Configure Grove-aware tool definitions** with proper scope annotations:
538 ```typescript
539 {
540 name: TOOL_GROVE_AWARE,
541 description: 'Tool that operates across Grove project boundaries',
542 cortex: {
543 guidance: 'Use for cross-project operations in Grove environments',
544 requiresGrove: true, // New Grove requirement flag
545 },
546 annotations: {
547 crossProjectHint: true, // Indicates Grove boundary crossing
548 },
549 }
550 ```
551
5524. **Test Grove project isolation** — verify tools respect Grove project boundaries and fail gracefully when Grove is not configured.
553
5545. **Document Grove migration impact** — note how tools behave differently in Grove vs traditional project structures.
555
556## Cross-Cutting Gotchas
557
558**Silent parameter drops**: When schema defines a parameter but handler ignores it, agents receive no error — their input is silently dropped. This is the most common drift failure.
559
560**Documentation lag**: Bundled SKILL.md becomes stale when handlers change. Always regenerate documentation after schema or handler modifications.
561
562**Cloud surface leakage**: Write operations must never leak to cloud MCP surface. Default to local-only; promote to cloud only with explicit read-only verification.
563
564**Validation vs runtime divergence**: Schema validation passes but handler expects different parameter structure. Test actual invocations, not just schema validation.
565
566**Collective conditional enablement**: `collective_*` tools are enabled by Collective connection state. Test both connected and disconnected scenarios across all transports.
567
568**Tool name consistency**: Use `myco_` prefix for standard tools, `collective_` prefix for Collective-dependent tools. Avoid generic names that conflict with other MCP servers.
569
570**Handler signature mismatch**: All handlers must accept `(input, client)` parameters. Missing DaemonClient parameter causes registration failures.
571
572**Cross-runtime schema compatibility**: OpenAI strict mode and Zod refinement patterns cause silent registration failures. Use plain JSON Schema types with descriptive documentation instead of complex validation constructs.
573
574**Skill tool registration gaps**: Skill lifecycle operations require complete tool registration (candidates, records, write_skill) — missing any component breaks agent workflows. Always register skill tools as a complete set.
575
576**Shared tool-runtime path shifts**: Tool definitions live in `packages/myco/src/tools/definitions.ts`. Update import paths and test references when the shared runtime moves.
577
578**Multi-transport registration complexity**: Shared tool-runtime requires consistent registration across MCP stdio, HTTP MCP, and CLI transports. Test all transports when adding new tools.
579
580**Transport-specific error handling**: Different transports expect different response formats. Implement transport-aware error formatting to prevent agent confusion.
581
582**ToolFailure anti-pattern**: Never throw raw Error objects from handlers — always wrap with ToolFailure interface for consistent agent error handling. Missing structured error context causes agent confusion across all transports.
583
584**Code duplication across tool surface**: Avoid copy-pasting handler patterns between tools. Extract shared utilities to `packages/myco/src/tools/shared.ts` and import consistently. Duplicated validation logic, error handling, and response formatting patterns create maintenance burden and drift risks across the unified tool surface. Use composition patterns instead:
585```typescript
586// BAD: Duplicated validation across multiple tools
587function handleToolA(input) {
588 if (!input.session_id || !isValidUuid(input.session_id)) {
589 throw new ToolFailure('Invalid session_id');
590 }
591 // ... tool logic
592}
593
594function handleToolB(input) {
595 if (!input.session_id || !isValidUuid(input.session_id)) {
596 throw new ToolFailure('Invalid session_id');
597 }
598 // ... tool logic (nearly identical validation)
599}
600
601// GOOD: Shared validation utilities
602import { validateSessionId, validateRequiredString } from './shared.js';
603
604function handleToolA(input) {
605 validateSessionId(input.session_id);
606 // ... tool logic
607}
608
609function handleToolB(input) {
610 validateSessionId(input.session_id);
611 // ... tool logic
612}
613```
614
615**Grove context injection failures**: Tools accessing project context must handle Grove migration gracefully. Missing Grove-aware context injection causes failures in Grove environments while working in traditional project structures, creating environment-specific bugs.