New Ory Agent Security is now live! Claim your complimentary test drive. Get Started!

Skip to main content

Build an MCP server with TypeScript

Build a local MCP server with two tools: get_ticket reads a ticket and close_ticket closes it. Connect an MCP client and use both tools before adding authentication. Tickets stay in memory until the server restarts.

This is the first step of Build and secure MCP servers with TypeScript.

Get the example​

Install Git, Node.js 22.19 or later, and npm. Use a terminal running Bash or Zsh for setup and running the server.

Clone the examples once. If you already cloned them for another MCP guide, stop its server, remove its client connection, and return to the ory-docs repository root instead of cloning again:

git clone --depth 1 --filter=blob:none --sparse --branch master \
https://github.com/ory/docs.git ory-docs
cd ory-docs
git sparse-checkout set code-examples/mcp-auth

Open the complete example and select its directory. The commands also create a private .env file for this example's settings:

cd code-examples/mcp-auth/typescript/baseline
umask 077
touch .env

Keep this directory open in your server terminal. Run the remaining commands here; each example has its own configuration.

Install the dependencies:

npm install

The example includes package.json, tsconfig.json, and .gitignore. The configuration supports TypeScript checks and keeps local credential files out of source control.

Add an in-memory ticket database​

tickets.ts starts with two open tickets. The complete file shows the ticket shape, initial data, and both operations. Reads return a copy; closing updates the stored ticket:

tickets.ts
// Module-level data preserves ticket changes across request-local server instances.
const tickets = [
{ id: "T-100", title: "Reset dashboard password", status: "open" },
{ id: "T-200", title: "Review invoice", status: "open" },
]

export function getTicket(id: string) {
const ticket = tickets.find((ticket) => ticket.id === id)
return ticket ? { ...ticket } : undefined
}

export function closeTicket(id: string) {
const ticket = tickets.find((ticket) => ticket.id === id)
if (!ticket) return undefined
ticket.status = "closed"
return { ...ticket }
}

Register and serve two MCP tools​

server.ts registers get_ticket and close_ticket. The SDK checks the ticket ID before the handler runs:

server.ts
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server"
import { toNodeHandler } from "@modelcontextprotocol/node"
import { createMcpExpressApp } from "@modelcontextprotocol/express"
import * as z from "zod"
import { getTicket, closeTicket } from "./tickets.js"

const failure = (text: string) => ({
isError: true,
content: [{ type: "text" as const, text }],
})

function createTicketServer() {
const server = new McpServer({ name: "ticket-tools", version: "1.0.0" })

server.registerTool(
"get_ticket",
{
description: "Read a ticket.",
inputSchema: z.object({ id: z.string().min(1) }),
},
async ({ id }) => {
const ticket = getTicket(id)
if (!ticket) return failure("Ticket not found")

return { content: [{ type: "text", text: JSON.stringify(ticket) }] }
},
)

server.registerTool(
"close_ticket",
{
description: "Close a ticket.",
inputSchema: z.object({ id: z.string().min(1) }),
},
async ({ id }) => {
const ticket = closeTicket(id)
if (!ticket) return failure("Ticket not found")

return { content: [{ type: "text", text: JSON.stringify(ticket) }] }
},
)

return server
}

const mcpHandler = createMcpHandler(createTicketServer)
const handler = toNodeHandler(mcpHandler)

const app = createMcpExpressApp({ host: "127.0.0.1" })
app.all("/mcp", (req, res) => void handler(req, res, req.body))

app
.listen(18480, "127.0.0.1")
.on("listening", () => console.log("MCP: http://127.0.0.1:18480/mcp"))

The close_ticket handler uses the same input schema and calls closeTicket. Both tools return isError: true with Ticket not found if the ticket does not exist.

The server listens on 127.0.0.1, which is accessible only from your machine. The Express adapter handles JSON and checks the request's Host and Origin headers. In your server terminal, check and start the application:

npm run check
npm start

It prints MCP: http://127.0.0.1:18480/mcp. Keep this process running.

Connect your MCP client​

Keep the server running. Open another terminal window or tab for your MCP client. This is your client terminal. Change to the ory-docs/code-examples/mcp-auth/typescript/baseline directory and choose one client:

With Claude Code installed and signed in, register the HTTP endpoint and start a new conversation:

claude mcp add --transport http --scope local ticket-tutorial http://127.0.0.1:18480/mcp
claude

Run /mcp in the conversation. Confirm that ticket-tutorial is connected and exposes get_ticket and close_ticket. The local scope makes this connection available in this project. See Claude Code's MCP documentation for configuration details.

Read and close a ticket​

Ask your agent to perform each action below using ticket-tutorial, approving tool calls when prompted. In Inspector, choose the tool in Tools, enter id as T-100, and choose Execute Tool.

ActionToolExpected status
Read ticket T-100.get_ticketopen
Close ticket T-100.close_ticketclosed
Read ticket T-100 again.get_ticketclosed

Try reading T-999 to see Ticket not found. Restart the server to reset the tickets before trying another client.

Add authentication​

  1. In the client terminal, exit the client.
  2. In the server terminal, stop the server.
  3. In the client terminal, remove the connection with claude mcp remove ticket-tutorial or codex mcp remove ticket-tutorial.
  4. Add browser login for a person granting an agent access, or application API keys for automated jobs.

See the complete server example to compare files.