AI-native APIs · the signature feature

Write a route once. Get REST and an MCP tool.

Because every Apex route carries a strict, typed schema, exposing it to an AI is automatic. The framework carries the MCP transport — you install nothing extra. Next.js, Nuxt, Rails, and Laravel don't do this. It's the reason Apex exists.

One definition, two surfaces #

defineApexRoute takes a Zod input shape, a handler, and a description. That single definition drives REST validation and the MCP tool's inputSchema — no duplication, no drift.

server/api/add.ts

import { defineApexRoute } from '@apex-stack/core'
import { z } from 'zod'

export default defineApexRoute({
  method: 'POST',
  description: 'Add two numbers and return their sum', // → MCP tool description
  input: { a: z.number(), b: z.number() },            // → REST validation + MCP inputSchema
  mcp: true,                                          // opt in to MCP exposure
  handler: ({ input }) => ({ sum: input.a + input.b }),
})

You now have both surfaces from that one file:

defineApexRoute
server/api/add.ts
↓ generates ↓
REST endpoint
POST /api/add {a,b} → { "sum": 5 }
MCP tool
tools/call add {a,b} → { "sum": 5 }

Invalid input returns 400 with the Zod issues. Only mcp: true routes are exposed as tools — everything else stays REST-only.

defineApexRoute options #

OptionTypePurpose
method'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'HTTP method. Defaults to GET.
descriptionstringHuman + AI-readable. Becomes the MCP tool description.
inputZod raw shapeAn object of Zod validators. Drives REST validation and the MCP inputSchema.
mcpbooleanOpt in to MCP exposure. Defaults to false.
mcpNamestringOverride the tool name (must match ^[a-zA-Z0-9_-]{1,64}$). Defaults to a slug from the file path.
handler(ctx) => OutputThe implementation. ctx.input is validated; ctx.url is the raw request URL.
Keep handlers thin

A handler is an adapter — business logic belongs in a service so it's reusable beyond the route. The route's job is the schema-carrying contract that makes REST + MCP automatic.

For a GET route, input is read from the query string; for others, from the JSON body. Use z.coerce.* when a query value should become a number/boolean:

server/api/greet.ts

export default defineApexRoute({
  method: 'GET',
  description: 'Greet a person by name',
  input: { name: z.string() },
  mcp: true,
  handler: ({ input }) => ({ message: `Hello, ${input.name}!` }),
})

The /mcp endpoint #

Apex serves all mcp: true routes at a single /mcp endpoint using the official MCP SDK's Web-standard Streamable HTTP transport, in stateless JSON mode — a fresh server per request, no session bookkeeping. It's available in apex dev and, in production, from apex build --server + apex start.

  • tools/list returns every exposed route as a tool, with its description and inputSchema.
  • tools/call <name> runs the same handler as the REST endpoint, over the same data.

The apex mcp inspector #

A built-in inspector connects to a running dev server's /mcp, lists tools with their signatures, and calls one — the local test loop for AI-callable routes, right in your terminal (it reuses the bundled MCP SDK, so there's nothing to install).

# list every tool and its input signature
apex mcp

# call one with JSON arguments
apex mcp --call add --args '{"a":2,"b":3}'

# point at a non-default endpoint
apex mcp --url http://localhost:4000/mcp

Full flags on the CLI reference. Smoke-testing tools with the inspector before shipping is the recommended rhythm.

Point Claude at your API #

Because /mcp speaks standard MCP over Streamable HTTP, any MCP client — Claude included — can connect to it and call your tools directly. Run your app, expose /mcp, and an assistant can read and write through the very same handlers your UI uses. No adapter, no second implementation, no drift between "the API" and "what the AI can do".

Resources are AI-native too #

The same principle scales to data. defineResource turns one table into list/get/create/update/delete over REST and as MCP tools (<name>_list, <name>_create, …) on one database — so an AI creating a row and a browser listing rows see the same data.

Design notes #

  • The schema is the enabler. The Zod shape is used for REST validation and the MCP inputSchema — one source of truth.
  • Opt-in & safe. MCP exposure is per-route (mcp: true). Free-form handlers without a schema aren't auto-exposed, by design.
  • Stateless. The /mcp transport runs in stateless JSON mode — a fresh server per request.
On the roadmap

Route-level auth/permission scopes for tools, outputSchema → MCP structured content, and extracting the MCP server into its own package to keep core lean.