Savvi Studio

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