← Back to Blog

Keeping Your API and MCP in Sync: The Single Source of Truth

By Zain • January 27, 2025

The future of application development is agent-driven. Users increasingly expect that anything they can do in your app, they should be able to do through an AI agent. But keeping your REST API and MCP (Model Context Protocol) server in perfect sync is a coordination nightmare—unless you define them from a single source of truth.

The Traditional Problem

Most teams manage API and MCP tools as separate concerns:

  • The REST API lives in one place with route handlers, middleware, and validation logic
  • The MCP server lives in another with tool definitions, schemas, and descriptions
  • Security policies get duplicated between the two
  • When you update the API, you remember (or forget) to update MCP
  • When you update MCP, the API diverges

This creates a dangerous situation: agents and users can access different functionality, security rules diverge, and you end up debugging "but it works in the API" vs "but the agent can't do it" issues.

Traditional separate API and MCP maintenance

The Ontology Solution

With ont-run, you define your functions once in a single ontology configuration:

export default defineOntology({
  functions: {
    approvePurchaseOrder: {
      description: 'Approve a purchase order (manager only)',
      access: ['manager'],
      inputs: z.object({
        orderId: z.string(),
        approvedBy: z.string(),
      }),
      outputs: z.object({
        status: z.enum(['approved', 'rejected']),
        timestamp: z.string().datetime(),
      }),
      resolver: approvePurchaseOrder,
    },
  },
});

From this single definition, ont-run automatically generates:

  1. REST API endpoint (POST /api/approvePurchaseOrder) with proper validation and authentication
  2. MCP tool that Claude and other agents can call
  3. Access control enforcement identical in both
  4. Exact same schema validation everywhere
  5. Interactive UI tools for human users

Single ontology generates REST, MCP, and UI tools

Why This Matters

Comparison of traditional vs ontology-driven approaches

1. Perfect Sync by Design

Your API and MCP tools are never out of sync because they're generated from the same source. Update the function definition once, and both the REST endpoint and MCP tool update automatically.

2. Unified Security

Access controls are defined in one place and enforced identically:

trackEvent: {
  access: ['public', 'admin'],  // Used for both API and MCP
  // ...
}

Whether a user calls your REST API or an agent calls the MCP tool, the same access rules apply. No more "admin can do it in MCP but not the API" bugs.

3. Single Schema Source

Your input/output schemas are defined once with Zod and used everywhere:

inputs: z.object({
  event: z.string().min(1).max(100),
  metadata: z.record(z.string()).optional(),
})

Invalid requests are caught with identical validation rules whether they come from REST or MCP. This prevents subtle bugs where MCP accepts something the API rejects (or vice versa).

4. Explicit Function Contracts

The ontology makes it crystal clear what your application does:

  • Who can call each function (access groups)
  • What data each function needs (inputs)
  • What data it returns (outputs)
  • Who wrote the function (resolver implementation)
  • What it affects (entities)

This becomes your API contract, your security audit trail, and your agent capability manifest—all the same thing.

5. Review Everything in One Place

Want to understand your application's entire API surface? Check your ontology.config.ts. Want to audit security? Look at access controls in one place. Want to see what agents can do? It's all there.

The Agent Integration Advantage

Users now have one consistent mental model:

┌─────────────────────────────┐
│   Ontology Definition       │
│  (Single Source of Truth)   │
└──────────────┬──────────────┘
               │
        ┌──────┴──────┐
        │             │
    ┌───▼────┐   ┌───▼────┐
    │REST API│   │MCP Tool│
    │        │   │        │
    │for Web │   │for AI  │
    │& Mobile│   │Agents  │
    └────────┘   └────────┘

Whether accessing through the web, mobile app, or AI agent, users get the same functionality, same validation, same access controls, same behavior.

Real-World Example: Purchase Order Management

Imagine you're building a supply chain platform with critical operations:

Create Purchase Order (buyers) — procurement team creates orders from suppliers Approve Purchase Order (managers only) — approval required before committing budget Escalate Order Delay (anyone) — flag when supplier is behind schedule

Define once:

export default defineOntology({
  functions: {
    createPurchaseOrder: {
      readOnly: false,
      description: 'Create a new purchase order with supplier',
      access: ['buyer', 'manager'],
      inputs: z.object({
        supplierId: z.string(),
        items: z.array(z.object({
          sku: z.string(),
          quantity: z.number().min(1),
          unitPrice: z.number().min(0),
        })),
        deliveryDate: z.string().datetime(),
      }),
      outputs: z.object({
        orderId: z.string(),
        totalAmount: z.number(),
        status: z.enum(['draft', 'pending_approval']),
      }),
      resolver: createPurchaseOrder,
    },

    approvePurchaseOrder: {
      readOnly: false,
      description: 'Approve a purchase order (manager only)',
      access: ['manager'],  // Automatically enforced everywhere
      inputs: z.object({
        orderId: z.string(),
        approvedBy: z.string(),
      }),
      outputs: z.object({
        status: z.enum(['approved', 'rejected']),
        timestamp: z.string().datetime(),
      }),
      resolver: approvePurchaseOrder,
    },

    escalateOrderDelay: {
      readOnly: false,
      description: 'Report a supplier delay and escalate to management',
      access: ['buyer', 'manager', 'warehouse'],
      inputs: z.object({
        orderId: z.string(),
        delayDays: z.number().min(1),
        reason: z.string(),
      }),
      outputs: z.object({
        escalationId: z.string(),
        notificationSent: z.boolean(),
      }),
      resolver: escalateOrderDelay,
    },
  },
});

Now you get:

  • Procurement Dashboard — REST endpoints for buyers to create orders
  • Mobile App — warehouse team uses same endpoints to check status
  • AI Agent — Claude can create draft orders, flag delays, or alert on bottlenecks
  • Securityaccess: ['manager'] on approve is enforced identically everywhere
  • Validation — same supplier/SKU/quantity rules apply whether API or agent calls it
  • Critical Workflow — approval process works the same way whether human or agent initiates it

Purchase order management: API and MCP working in sync

How to Implement This

  1. Define your ontology in ontology.config.ts with all your functions, schemas, and access controls
  2. Implement resolvers — the actual business logic for each function
  3. Deploy once — ont-run generates your REST API, MCP server, and frontend automatically
  4. Users access however they want — web, mobile, or through an AI agent

The framework handles the synchronization. You just define your application once.

The Future is Agent-Native

As AI agents become mainstream, users will expect them everywhere:

"I should be able to ask Claude to do anything I can do in the app"

With an ontology-driven approach, this expectation becomes a guarantee. Your API and MCP tools are synchronized by definition, not by discipline or luck.

The single source of truth makes agent integration not just possible, but the natural, inevitable consequence of defining your application once.


Ready to sync your API and MCP? Check out ont-run to build agent-native applications the right way.