Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@acme/api": "workspace:*",
"@acme/auth": "workspace:*",
"@acme/db": "workspace:*",
"@acme/mcp": "workspace:*",
"@acme/ui": "workspace:*",
"@acme/validators": "workspace:*",
"@t3-oss/env-nextjs": "^0.13.8",
Expand Down
12 changes: 12 additions & 0 deletions apps/nextjs/src/app/api/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createMcpHandler } from "@acme/mcp";

import { env } from "~/env";

const handler = createMcpHandler({
redisUrl: env.REDIS_URL,
basePath: "/api",
maxDuration: 60,
verboseLogs: env.NODE_ENV === "development",
});

export { handler as GET, handler as POST, handler as DELETE };
1 change: 1 addition & 0 deletions apps/nextjs/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const env = createEnv({
*/
server: {
POSTGRES_URL: z.url(),
REDIS_URL: z.string().url().optional(),
},

/**
Expand Down
13 changes: 13 additions & 0 deletions mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"acme-service": {
"url": "http://localhost:3000/api/mcp",
"env": {
"REDIS_URL": "${REDIS_URL}"
}
}
}
}



51 changes: 51 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# @acme/mcp

MCP (Model Context Protocol) handler package for the Next.js application.

## Overview

This package provides an MCP server implementation that enables AI assistants (Claude Desktop, Cursor, etc) to perform arithmetic operations via the MCP protocol.

## Features

- **MCP streaming endpoint**: MCP server using `mcp-handler` to stream responses via Server-Sent Events (SSE)

- **Arithmetic Tools**: Four example MCP tools for performing basic arithmetic operations:
- `add`, `subtract`, `multiply`, `divide`

- **SSE Resumability**: Optional Redis support for resumable Server-Sent Events (SSE) streams

## SSE Resumability with Redis

- If a serverless function times out or the connection drops mid-stream, Redis stores the last position
- A new function invocation can read from Redis and resume the stream from where it left off
- Without Redis, streams still work but cannot resume after interruptions - they must restart from the beginning

To enable Redis, set the `REDIS_URL` environment variable. The handler will automatically use it if provided. For local development, running Redis in a docker container is an easy path forward if you already have the docker toolchain setup.

## Integrating with AI Assistants

To use this MCP server with AI assistants like Claude Desktop or Cursor, configure it using the appropriate settings file.
Cursor: ~/.cursor/mcp.json
Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json

### Configuration

Update your `mcp.json` file to include your service name and URL. You can also pass environment variables to the MCP server.

```json
{
"mcpServers": {
"acme-service": {
"url": "http://localhost:3000/api/mcp", // Can change to deployed URL
"env": {
"REDIS_URL": "${REDIS_URL}" //Optional: enables SSE resumability
}
```

You might need to restart your AI assistant to load the new MCP server configuration.

## Dependencies

- Uses `zod` v3 (`^3.23.8`) for compatibility with `mcp-handler`
- The rest of the monorepo uses `zod` v4, so this package isolates the dependency to prevent conflicts
10 changes: 10 additions & 0 deletions packages/mcp/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "eslint/config";

import { baseConfig } from "@acme/eslint-config/base";

export default defineConfig(
{
ignores: ["dist/**"],
},
baseConfig,
);
34 changes: 34 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@acme/mcp",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
}
},
"license": "MIT",
"scripts": {
"build": "tsc",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"dev": "tsc",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"mcp-handler": "^1.0.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@acme/eslint-config": "workspace:*",
"@acme/prettier-config": "workspace:*",
"@acme/tsconfig": "workspace:*",
"@types/node": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
},
"prettier": "@acme/prettier-config"
}
182 changes: 182 additions & 0 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { createMcpHandler as createHandler } from "mcp-handler";
import { z } from "zod";

/**
* Creates an MCP handler with arithmetic tools.
*
* @param config - Configuration options
* @param config.redisUrl - Optional Redis URL for SSE resumability in serverless environments.
* When provided, Redis stores stream state (last sent message position) so that if a function
* times out or connection drops, streams can resume from where they left off rather than
* restarting. Without Redis, streams work but cannot resume after interruptions.
* @param config.basePath - Base path for the API route (default: "/api")
* @param config.maxDuration - Maximum duration for requests in seconds (default: 60)
* @param config.verboseLogs - Enable verbose logging (default: false, or true in development)
*/
export function createMcpHandler(config?: {
redisUrl?: string;
basePath?: string;
maxDuration?: number;
verboseLogs?: boolean;
}) {
return createHandler(
(server) => {
// Add two numbers
server.registerTool(
"add",
{
description: "Add two numbers together.",
inputSchema: {
a: z.number(),
b: z.number(),
},
},
({ a, b }: { a: number; b: number }) => {
const result = a + b;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
result,
operation: "add",
operands: { a, b },
},
null,
2,
),
},
],
};
},
);

// Subtract two numbers
server.registerTool(
"subtract",
{
description: "Subtract the second number from the first number.",
inputSchema: {
a: z.number(),
b: z.number(),
},
},
({ a, b }: { a: number; b: number }) => {
const result = a - b;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
result,
operation: "subtract",
operands: { a, b },
},
null,
2,
),
},
],
};
},
);

// Multiply two numbers
server.registerTool(
"multiply",
{
description: "Multiply two numbers together.",
inputSchema: {
a: z.number(),
b: z.number(),
},
},
({ a, b }: { a: number; b: number }) => {
const result = a * b;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
result,
operation: "multiply",
operands: { a, b },
},
null,
2,
),
},
],
};
},
);

// Divide two numbers
server.registerTool(
"divide",
{
description: "Divide the first number by the second number.",
inputSchema: {
a: z.number(),
b: z.number(),
},
},
({ a, b }: { a: number; b: number }) => {
if (b === 0) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
error: "Division by zero is not allowed",
},
null,
2,
),
},
],
isError: true,
};
}
const result = a / b;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
result,
operation: "divide",
operands: { a, b },
},
null,
2,
),
},
],
};
},
);
},
{
// Optional server options
},
{
// Redis URL for SSE resumability
redisUrl: config?.redisUrl ?? process.env.REDIS_URL,
basePath: config?.basePath ?? "/api",
maxDuration: config?.maxDuration ?? 60,
verboseLogs:
config?.verboseLogs ?? process.env.NODE_ENV === "development",
},
);
}
8 changes: 8 additions & 0 deletions packages/mcp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@acme/tsconfig/compiled-package.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["src"],
"exclude": ["node_modules"]
}
Loading