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:
// 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:
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:
- Claude Code
- Codex
- MCP Inspector
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.
With Codex installed and signed in, register the same HTTP endpoint and start a new conversation:
codex mcp add ticket-tutorial --url http://127.0.0.1:18480/mcp
codex
Run /mcp in the conversation to confirm that ticket-tutorial exposes both tools. Codex saves this connection in its MCP
configuration. See Codex's MCP documentation for configuration details.
MCP Inspector is a browser interface for connecting to an MCP server and calling its tools directly, without an AI conversation. These settings keep it on loopback with temporary state and secrets in memory:
cat > inspector.json <<'JSON'
{"mcpServers":{"tickets":{"type":"http","url":"http://127.0.0.1:18480/mcp"}}}
JSON
export INSPECTOR_STATE=$(mktemp -d)
HOST=127.0.0.1 CLIENT_PORT=6274 MCP_STORAGE_DIR="$INSPECTOR_STATE" \
MCP_INSPECTOR_SECRET_STORE=memory \
npm exec --yes --package @modelcontextprotocol/[email protected] -- \
mcp-inspector --web --config inspector.json --server tickets
- Open the local URL printed by Inspector.
- Turn on the tickets connection switch, then open Tools.
- Confirm that
get_ticketandclose_ticketappear.
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.
| Action | Tool | Expected status |
|---|---|---|
| Read ticket T-100. | get_ticket | open |
| Close ticket T-100. | close_ticket | closed |
| Read ticket T-100 again. | get_ticket | closed |
Try reading T-999 to see Ticket not found. Restart the server to reset the tickets before trying another client.
Add authentication
- In the client terminal, exit the client.
- In the server terminal, stop the server.
- In the client terminal, remove the connection with
claude mcp remove ticket-tutorialorcodex mcp remove ticket-tutorial. - 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.