cd /news/ai-agents/expose-next-js-functions-as-mcp-tool… · home › topics › ai-agents › article
[ARTICLE · art-153009] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Expose Next.js functions as MCP tools with Dendra

A developer built Dendra, an open source MIT-licensed TypeScript framework that lets a single procedure definition—input schema plus handler—be exposed both as a Next.js API route and as a Model Context Protocol tool. In a walkthrough using Dendra 0.7.1 and the MCP TypeScript SDK 1.32.1, an addition procedure is served through a Next.js route and a local stdio MCP server sharing the same handler, with input validation rejecting non-numeric values with HTTP 400 and procedures lacking an explicit .ai() declaration excluded from the generated tool list.

by read7 min views11 publishedOct 19, 2026

Expose a Next.js operation as an MCP tool with Dendra, sharing its input validation and handler.

Your application already knows how to look up a record, calculate a result or create a task. When you add an AI assistant, those same operations need a tool name, an input schema and a way to execute them.

I built Dendra, the open source framework used in this example, to keep those definitions together.

Dendra is an open source TypeScript framework for typed APIs and explicitly enabled AI tools. A procedure contains the input contract and handler. Adapters connect that procedure to the interfaces your application needs, including Next.js API routes and Model Context Protocol tools.

In this tutorial, we will expose one small operation through a Next.js route and a local MCP server. Both will use the same handler. We will also verify what happens when a tool receives invalid input or tries to call a procedure that has not been enabled for AI access.

The example uses addition so the connection between the two interfaces is easy to inspect. The same structure can hold application logic such as searching your own records or calculating a project estimate.

There are three pieces:

The MCP server in this tutorial uses local standard input and output, usually called stdio. It runs as a separate process beside the Next.js application. A remote MCP endpoint requires additional HTTP transport and authentication setup.

Start in an existing Next.js App Router project with TypeScript and Node.js 22 or later. The paths below assume an app directory at the project root. If your project uses src/app, place the router in src/server and adjust the MCP server import accordingly.

This walkthrough uses Dendra 0.7.1 and the MCP TypeScript SDK 1.32.1:

npm install dendra@0.7.1 @modelcontextprotocol/sdk@1.32.1
npm install --save-dev tsx@4.23.15

Use a supported, patched Next.js release for your application. The example adds Dendra and the MCP SDK to your existing project; it does not change your Next.js version.

Dendra is released under the MIT license and is currently before version 1.0. Pinning the version makes this walkthrough reproducible while the framework develops.

Create server/router.ts:

import { initDendra, s, type RouterRecord } from "dendra";

const { route } = initDendra<Record<string, never>>();

export const appRouter = {
  add: route
    .input(s.object({ a: s.number(), b: s.number() }))
    .ai({ description: "Add two numbers" })
    .handler(({ input }) => input.a + input.b),

  health: route.get().handler(() => ({ ok: true })),
} satisfies RouterRecord;

export type AppRouter = typeof appRouter;

The add procedure has a numeric input schema, a tool description and one handler. Calling .ai() explicitly includes it in the generated tool list.

The health procedure is available through the application API. It has no .ai() declaration, so the MCP bridge excludes it. We will test that boundary later.

The empty context in this example is intentional: adding two numbers does not access a user account or private data. Operations that do need access controls should receive a verified user context and enforce permissions in middleware and business logic.

Create app/rpc/[...path]/route.ts:

import { toNextHandler } from "dendra";
import { appRouter } from "../../../server/router";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export const { GET, POST } = toNextHandler(appRouter, {
  prefix: "/rpc/",
  createContext: () => ({}),
});

Start your Next.js application with its usual development command. With the default port of 3000, call the operation:

curl http://localhost:3000/rpc/add \
  -H 'Content-Type: application/json' \
  -d '{"a":20,"b":22}'

The response is:

{"result":42}

If you change a to the string "twenty", input validation rejects the request with HTTP 400. The handler expects numbers on every supported execution path.

Create mcp-server.mts at the project root:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { createMcpBridge } from "dendra";
import { appRouter } from "./server/router";

const bridge = createMcpBridge(appRouter, {});
const server = new Server(
  { name: "dendra-tutorial", version: "1.0.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: bridge.listTools(),
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const result = await bridge.callTool(
    request.params.name,
    request.params.arguments,
  );
  return { content: result.content, isError: result.isError };
});

await server.connect(new StdioServerTransport());

createMcpBridge derives the tool definitions from the router and executes calls through Dendra. The official MCP SDK handles the protocol connection over stdio.

The tool names here are simple top level names. If you later expose nested router paths, check the tool naming requirements of your MCP clients and use a collision checked mapping where necessary.

The Next.js route and this MCP server import the same router. They run in separate processes, so shared in memory state would not be synchronized between them. For a real application with stored data, connect both to the appropriate database or use a server architecture that routes calls to the same application service.

Create verify-mcp.mts at the project root:

import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: process.execPath,
  args: ["--import", "tsx", fileURLToPath(new URL("./mcp-server.mts", import.meta.url))],
});
const client = new Client({ name: "dendra-tutorial-check", version: "1.0.0" });

try {
  await client.connect(transport);
  const tools = (await client.listTools()).tools;
  assert.deepEqual(tools.map((tool) => tool.name), ["add"]);
  console.log("Tools: add");

  const result = await client.callTool({ name: "add", arguments: { a: 20, b: 22 } });
  assert.notEqual(result.isError, true);
  assert.deepEqual(result.content, [{ type: "text", text: "42" }]);
  console.log("Valid call: 42");

  const invalid = await client.callTool({ name: "add", arguments: { a: "twenty", b: 22 } });
  assert.equal(invalid.isError, true);
  console.log("Invalid argument: rejected");

  const unexposed = await client.callTool({ name: "health", arguments: {} });
  assert.equal(unexposed.isError, true);
  console.log("Unexposed procedure: rejected");
} finally {
  await client.close();
}

Run it from the project root:

npx tsx verify-mcp.mts

Expected output:

Tools: add
Valid call: 42
Invalid argument: rejected
Unexposed procedure: rejected

This check uses an MCP client to launch the server, list its tools and make calls. It does not require an AI model subscription or API key. The Next.js server can also be stopped during this check because the MCP process imports the procedure directly.

To use the server with a desktop MCP client, configure that client to launch your Node.js executable with --import, the absolute path to the installed tsx and the absolute path to mcp-server.mts. Follow your client's local server configuration format. Keep application logs off standard output because stdio uses that channel for protocol messages.

The .ai() declaration controls whether a procedure is exposed as a tool. Input validation checks the arguments. Authentication establishes who is calling, and authorization decides what that caller may do.

Dendra lets you attach middleware with .use() and supply context to the execution path. Your application must implement the policy and pass the correct context for each user. Do not carry the empty context from this arithmetic example into a tool that reads private records or changes customer data.

For remote access, use an appropriate MCP transport, HTTPS and authentication, with permissions tied to the caller. The local stdio example does not implement those deployment concerns.

The HTTP request and MCP tool call use one input schema and handler. Both reject the same invalid argument. The tool list contains add, while the unexposed health procedure stays out of the MCP interface.

The distinction matters: selecting a procedure with .ai() controls tool exposure. It does not establish a user's identity, grant permission, synchronize separate processes, or deploy a remote server.

Dendra also has adapters for typed clients, REST routing and OpenAPI generation. They are outside this example. The HTTP route shown here is an RPC endpoint; it is not an MCP endpoint.

The example was checked on 19 October 2026 using a clean dependency installation with Dendra 0.7.1, MCP SDK 1.32.1, TypeScript 5.9.3 and tsx 4.23.15. The local Next.js application used Next.js 16.4.2 and React 19.3.1.

Type checking, MCP initialization, tool listing, valid execution, invalid input rejection and rejection of an unexposed procedure passed. Requests to the running Next.js development server also returned the expected HTTP results. These checks cover this small local example, not a deployed application's authentication or security.

I created Dendra. AI assisted with drafting and checking this tutorial. The example was verified through executable TypeScript, HTTP and MCP tests.

── more in #ai-agents 4 stories · sorted by recency
── more on @dendra 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/expose-next-js-funct…] indexed:0 read:7min 2026-10-19 · —