MCP Tool Contracts
The @savvi-studio/tool-contracts package provides a declarative contract framework for Studio MCP tools. Contracts formalise what a tool requires (preconditions), guarantees (postconditions), depends on, and is allowed to touch (capabilities).
Package: packages/tool-contracts
Status: Implemented (Phase 2A, April 2026)
Why Tool Contracts?
Without contracts, MCP tools are procedural facades — they accept inputs, run, and return outputs, but nothing enforces:
- that required inputs are present and valid before execution
- that the outputs satisfy expected shape or invariants after execution
- that tools are invoked in the right dependency order
- that a given agent role is allowed to call a given tool
Contracts solve all four. An agent (or orchestrator) that understands contracts can plan tool sequences, reject invalid inputs early, verify outputs, and enforce capability boundaries — without needing to run the tool to discover failures.
Core Types
SemanticType
A refinement on primitive types that carries domain meaning. Used to annotate input and output fields beyond raw Zod validation.
type SemanticType =
| 'ltree-path' // PostgreSQL ltree hierarchical path
| 'resource-id' // Graph resource identifier (Snowflake ID)
| 'module-ref' // Module reference: "namespace/name"
| 'uuid' // RFC 4122 UUID
| 'semver' // Semantic version string
| 'file-path' // Absolute or relative filesystem path
| 'json-object' // Arbitrary JSON object
| 'unknown'; // No semantic annotation
CapabilityTag
Tags that classify what a tool does. Used for RBAC decisions, sandbox enforcement, and LLM planning:
type CapabilityTag =
| 'READ' // reads existing data
| 'WRITE' // writes or updates data
| 'DELETE' // permanently removes data
| 'CREATE' // creates new data
| 'EXECUTE' // runs an operation
| 'ADMIN' // administrative operations
| 'SCHEMA_MUTATION' // alters schema definitions
| 'CODE_EXECUTION'; // executes generated or user code
PreconditionPredicate
A guard evaluated before tool execution. If any precondition returns false, the tool is not called and the error code is returned to the caller.
interface PreconditionPredicate {
description: string; // human-readable explanation
errorCode: string; // e.g. "INVALID_MODULE_REF"
evaluate: (context: Record<string, unknown>) => boolean;
}
PostconditionPredicate
An assertion evaluated after tool execution. Verifies the output satisfies expected invariants.
interface PostconditionPredicate {
description: string;
errorCode: string;
evaluate: (context: Record<string, unknown>) => boolean;
}
DeclaredToolContract
The full contract for a single tool:
interface DeclaredToolContract {
toolName: string;
description: string;
inputSchema: z.ZodSchema;
outputSchema: z.ZodSchema;
// Semantic annotations on fields (optional)
inputSemanticTypes?: Record<string, SemanticType>;
outputSemanticTypes?: Record<string, SemanticType>;
// Guards evaluated before/after execution (optional)
preconditions?: PreconditionPredicate[];
postconditions?: PostconditionPredicate[];
// Other tools that must be registered and reachable
dependencies?: string[];
// What this tool is allowed to do — used for RBAC
capabilities: CapabilityTag[];
// Optional resource limits for sandbox execution
resourceLimits?: {
timeoutMs: number;
memoryMb: number;
maxOutputBytes: number;
};
}
ToolDependencyGraph
A registry of contracts with dependency validation. Detects circular references and ensures all declared dependencies are registered.
interface ToolDependencyGraph {
tools: Map<string, DeclaredToolContract>;
dependencies: Map<string, Set<string>>;
validate(): string[]; // returns error messages; empty = valid
}
Usage
Registering a contract
import {
createToolDependencyGraph,
registerToolContract,
createPrecondition,
createPostcondition,
type DeclaredToolContract,
} from '@savvi-studio/tool-contracts';
import { z } from 'zod';
const graph = createToolDependencyGraph();
const compileContract: DeclaredToolContract = {
toolName: 'modules.compile',
description: 'Compile a module manifest into a validated bundle',
inputSchema: z.object({ moduleRef: z.string() }),
outputSchema: z.object({ bundlePath: z.string(), diagnostics: z.array(z.unknown()) }),
inputSemanticTypes: { moduleRef: 'module-ref' },
outputSemanticTypes: { bundlePath: 'file-path' },
preconditions: [
createPrecondition(
'moduleRef must be namespace/name format',
'INVALID_MODULE_REF_FORMAT',
(ctx) => {
const ref = ctx.moduleRef as string | undefined;
if (!ref) return false;
const parts = ref.split('/');
return parts.length === 2 && parts.every((p) => p.length > 0);
},
),
],
postconditions: [
createPostcondition(
'bundlePath must be a non-empty string',
'MISSING_BUNDLE_PATH',
(ctx) => typeof ctx.bundlePath === 'string' && ctx.bundlePath.length > 0,
),
],
dependencies: ['modules.validate'],
capabilities: ['READ', 'EXECUTE', 'CODE_EXECUTION'],
resourceLimits: { timeoutMs: 30_000, memoryMb: 512, maxOutputBytes: 10_485_760 },
};
registerToolContract(graph, compileContract);
Evaluating preconditions
import { evaluatePreconditions } from '@savvi-studio/tool-contracts';
const result = evaluatePreconditions(contract.preconditions, {
moduleRef: 'savvi-studio/teams',
});
if (!result.passed) {
// result.errors: Array<{ code: string; message: string }>
return { error: result.errors[0] };
}
Validating the dependency graph
const errors = graph.validate();
// errors = [] → all registered, no cycles
// errors = ["Circular dependency detected in tool: toolA"] → fix before deploy
Execution Steps (Closed Feedback Loop)
Alongside tool contracts, the studio-mcp runtime records execution steps that agents can query between tool calls. This closes the feedback loop for iterative agent workflows.
Schema (in packages/studio-mcp/src/contracts/tools.ts):
interface ExecutionStep {
stepId: string;
index: number;
toolName: string;
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
durationMs: number;
statusCode: number;
emittedAt: string; // ISO 8601
}
Query: Call modules.query_state with a traceId — the response includes previousSteps: ExecutionStep[].
Agent loop pattern:
1. Call modules.query_state(traceId) → previousSteps: []
2. Call modules.compile(moduleRef)
3. Call modules.query_state(traceId) → previousSteps: [{ toolName: "modules.compile", ... }]
4. Inspect outputs, decide next step
5. Continue or exit
Relationship to Studio MCP Tools
The current tool inventory and contract status:
| Tool | Contract Status | Capabilities |
|---|---|---|
modules.list |
Implicit only | READ |
modules.inspect |
Implicit only | READ |
modules.validate |
Implicit only | READ |
modules.compile |
Planned | READ, EXECUTE, CODE_EXECUTION |
modules.bundle |
Planned | READ, EXECUTE |
modules.codegen |
Planned | READ, EXECUTE, CODE_EXECUTION |
modules.schema |
Planned | READ |
graph.install_plan |
Planned | READ, WRITE, SCHEMA_MUTATION |
system.codegen.all |
Planned | READ, EXECUTE, CODE_EXECUTION, SCHEMA_MUTATION |
studio.health |
Implicit only | READ |
"Planned" means the tool exists but has not yet been wired to a DeclaredToolContract. Friday 4/5 integration work connects modules.compile first.
See Also
- Module System — Modules that tools operate on
- Graph Architecture — Graph substrate underlying all operations
- packages/tool-contracts — Package source
- packages/studio-mcp — MCP server that hosts these tools
- PLATFORM_GAP_ANALYSIS_20260401.md — Full AI-first gap roadmap