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
23 changes: 16 additions & 7 deletions core-web/apps/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Before setting up the MCP server, you need these environment variables to connec
| ------------ | -------- | ---------------------------------- | ------- |
| `DOTCMS_URL` | ✅ | Your dotCMS instance URL | `https://demo.dotcms.com` |
| `AUTH_TOKEN` | ✅ | API authentication token (created in [setup step](#create-a-dotcms-api-token)) | `your-api-token-here` |
| `PORT` | ❌ | HTTP server port (defaults to 3000) | `3000` |
| `VERBOSE` | ❌ | Enable detailed logging for troubleshooting | `true` |
| `RESPONSE_MAX_LENGTH` | ❌ | Maximum character limit for response truncation (no truncation if not set) | `5000` |

Expand Down Expand Up @@ -122,7 +123,7 @@ Get up and running with the dotCMS MCP Server in minutes:

### Claude Desktop Setup

Add the MCP server to your Claude Desktop configuration file. The configuration file location varies by operating system:
The MCP server now runs as an HTTP server using Streamable HTTP transport. Add the server to your Claude Desktop configuration file. The configuration file location varies by operating system:

- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Expand All @@ -135,7 +136,8 @@ Add the MCP server to your Claude Desktop configuration file. The configuration
"args": ["-y", "@dotcms/mcp-server"],
"env": {
"DOTCMS_URL": "https://your-dotcms-instance.com",
"AUTH_TOKEN": "your-auth-token"
"AUTH_TOKEN": "your-auth-token",
"PORT": "3000"
}
}
}
Expand All @@ -154,7 +156,8 @@ Add the MCP server to your Cursor configuration. Open Cursor Settings and naviga
"args": ["-y", "@dotcms/mcp-server"],
"env": {
"DOTCMS_URL": "https://your-dotcms-instance.com",
"AUTH_TOKEN": "your-auth-token"
"AUTH_TOKEN": "your-auth-token",
"PORT": "3000"
}
}
}
Expand Down Expand Up @@ -284,10 +287,14 @@ yarn nx build mcp-server

#### 2. Use MCP Inspector for debug

After a succesful build
After a successful build, start the HTTP server and connect the MCP Inspector:

```bash
npx @modelcontextprotocol/inspector -e DOTCMS_URL=https://demo.dotcms.com -e AUTH_TOKEN=the-auth-token node dist/apps/mcp-server
# Start the server (it will listen on http://localhost:3000/mcp by default)
DOTCMS_URL=https://demo.dotcms.com AUTH_TOKEN=the-auth-token node dist/apps/mcp-server/main.js

# In another terminal, connect the MCP Inspector
npx @modelcontextprotocol/inspector http://localhost:3000/mcp
```

#### 3. Use Local Build in AI Assistants
Expand All @@ -301,7 +308,8 @@ npx @modelcontextprotocol/inspector -e DOTCMS_URL=https://demo.dotcms.com -e AUT
"args": ["/path/to/dotcms/core/core-web/dist/apps/mcp-server/main.js"],
"env": {
"DOTCMS_URL": "your-dotcms-url",
"AUTH_TOKEN": "your-auth-token"
"AUTH_TOKEN": "your-auth-token",
"PORT": "3000"
}
}
}
Expand All @@ -317,7 +325,8 @@ npx @modelcontextprotocol/inspector -e DOTCMS_URL=https://demo.dotcms.com -e AUT
"args": ["/path/to/dotcms/core/core-web/dist/apps/mcp-server/main.js"],
"env": {
"DOTCMS_URL": "your-dotcms-url",
"AUTH_TOKEN": "your-auth-token"
"AUTH_TOKEN": "your-auth-token",
"PORT": "3000"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions core-web/apps/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.1",
"@types/express": "^5.0.0",
"express": "^4.21.2",
"zod": "^3.25.67"
},
"publishConfig": {
Expand Down
131 changes: 110 additions & 21 deletions core-web/apps/mcp-server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import express from 'express';
import { z } from 'zod';

import { randomUUID } from 'node:crypto';

import { registerContentTypeTools } from './tools/content-types';
import { registerContextTools } from './tools/context';
import { registerSearchTools } from './tools/search';
import { registerWorkflowTools } from './tools/workflow';
import { createContextCheckingServer } from './utils/context-checking-server';

const originalServer = new McpServer({
name: 'DotCMS',
version: '1.0.0'
});

// Create context-checking server proxy to enforce initialization requirements
const server = createContextCheckingServer(originalServer);

const DOTCMS_URL = process.env.DOTCMS_URL;
const AUTH_TOKEN = process.env.AUTH_TOKEN;
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;

const urlSchema = z.string().url();
const tokenSchema = z.string().min(1, 'AUTH_TOKEN cannot be empty');
Expand All @@ -31,19 +28,111 @@ try {
process.exit(1);
}

// Register context tools first (context_initialization is exempt from checking)
registerContextTools(server);
const app = express();
app.use(express.json());

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

// Create and configure MCP server
function createMcpServer() {
const originalServer = new McpServer({
name: 'DotCMS',
version: '1.0.0'
});

// Register content type tools (will be protected by context checking)
registerContentTypeTools(server);
// Create context-checking server proxy to enforce initialization requirements
const server = createContextCheckingServer(originalServer);

// Register context tools first (context_initialization is exempt from checking)
registerContextTools(server);

// Register content type tools (will be protected by context checking)
registerContentTypeTools(server);

// Register search tools (will be protected by context checking)
registerSearchTools(server);

// Register workflow tools (will be protected by context checking)
registerWorkflowTools(server);

return server;
}

// Register search tools (will be protected by context checking)
registerSearchTools(server);
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

// Register workflow tools (will be protected by context checking)
registerWorkflowTools(server);
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports[sessionId] = transport;
}
// DNS rebinding protection is disabled for local development
// For production deployments, enable it with:
// enableDnsRebindingProtection: true,
// allowedHosts: ['your-domain.com'],
// allowedOrigins: ['https://your-domain.com']
});

const transport = new StdioServerTransport();
(async () => {
await server.connect(transport);
})();
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};

const server = createMcpServer();

// Connect to the MCP server
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided'
},
id: null
});
return;
}

// Handle the request
await transport.handleRequest(req, res, req.body);
});

// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (
req: express.Request,
res: express.Response
) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}

const transport = transports[sessionId];
await transport.handleRequest(req, res);
};

// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);

// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);

app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`DotCMS MCP Server listening on http://localhost:${PORT}/mcp`);
});
Loading