Files
gh-jezweb-claude-skills-ski…/templates/resource-server.ts
2025-11-30 08:25:43 +08:00

316 lines
7.5 KiB
TypeScript

/**
* Resource-Server MCP Template
*
* An MCP server focused on exposing resources (static and dynamic data).
* Resources are URI-addressed data that LLMs can query.
*
* Setup:
* 1. npm install @modelcontextprotocol/sdk hono zod
* 2. npm install -D @cloudflare/workers-types wrangler typescript
* 3. Configure bindings in wrangler.jsonc (D1, KV, etc.)
* 4. wrangler deploy
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
import { Hono } from 'hono';
type Env = {
DB?: D1Database;
CACHE?: KVNamespace;
};
const server = new McpServer({
name: 'resource-server',
version: '1.0.0'
});
// Resource 1: Static Configuration
server.registerResource(
'config',
new ResourceTemplate('config://app', { list: undefined }),
{
title: 'Application Configuration',
description: 'Returns application configuration and metadata'
},
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
name: 'resource-server',
version: '1.0.0',
features: ['static-resources', 'dynamic-resources', 'd1-integration'],
lastUpdated: new Date().toISOString()
}, null, 2)
}]
})
);
// Resource 2: Environment Info
server.registerResource(
'environment',
new ResourceTemplate('env://info', { list: undefined }),
{
title: 'Environment Information',
description: 'Returns environment and deployment information'
},
async (uri, _, env) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
hasDatabase: !!env.DB,
hasCache: !!env.CACHE,
runtime: 'cloudflare-workers',
timestamp: new Date().toISOString()
}, null, 2)
}]
})
);
// Resource 3: Dynamic User Profile (with parameter)
server.registerResource(
'user-profile',
new ResourceTemplate('user://{userId}', { list: undefined }),
{
title: 'User Profile',
description: 'Returns user profile data by ID'
},
async (uri, { userId }, env) => {
if (!env.DB) {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ error: 'Database not configured' }, null, 2)
}]
};
}
try {
const user = await env.DB
.prepare('SELECT id, name, email, created_at FROM users WHERE id = ?')
.bind(userId)
.first();
if (!user) {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ error: `User ${userId} not found` }, null, 2)
}]
};
}
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(user, null, 2)
}]
};
} catch (error) {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
error: `Failed to fetch user: ${(error as Error).message}`
}, null, 2)
}]
};
}
}
);
// Resource 4: Dynamic Data by Key (KV-backed)
server.registerResource(
'data',
new ResourceTemplate('data://{key}', { list: undefined }),
{
title: 'Data by Key',
description: 'Returns data from KV store by key'
},
async (uri, { key }, env) => {
if (!env.CACHE) {
return {
contents: [{
uri: uri.href,
mimeType: 'text/plain',
text: 'KV store not configured'
}]
};
}
try {
const value = await env.CACHE.get(key as string);
if (!value) {
return {
contents: [{
uri: uri.href,
mimeType: 'text/plain',
text: `Key "${key}" not found`
}]
};
}
// Try to parse as JSON, fallback to plain text
let mimeType = 'text/plain';
let text = value;
try {
JSON.parse(value);
mimeType = 'application/json';
} catch {
// Not JSON, keep as plain text
}
return {
contents: [{
uri: uri.href,
mimeType,
text
}]
};
} catch (error) {
return {
contents: [{
uri: uri.href,
mimeType: 'text/plain',
text: `Error fetching key: ${(error as Error).message}`
}]
};
}
}
);
// Resource 5: List All Users (D1-backed)
server.registerResource(
'users-list',
new ResourceTemplate('users://all', { list: undefined }),
{
title: 'All Users',
description: 'Returns list of all users from database'
},
async (uri, _, env) => {
if (!env.DB) {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ error: 'Database not configured' }, null, 2)
}]
};
}
try {
const result = await env.DB
.prepare('SELECT id, name, email FROM users ORDER BY created_at DESC LIMIT 100')
.all();
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
count: result.results.length,
users: result.results
}, null, 2)
}]
};
} catch (error) {
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
error: `Failed to fetch users: ${(error as Error).message}`
}, null, 2)
}]
};
}
}
);
// Resource 6: Dynamic File Content (with path parameter)
server.registerResource(
'file',
new ResourceTemplate('file://{path}', { list: undefined }),
{
title: 'File Content',
description: 'Returns content of a file by path (simulated)'
},
async (uri, { path }) => {
// In production, you might fetch from R2, D1, or external storage
const simulatedFiles: Record<string, string> = {
'readme.md': '# README\n\nWelcome to the resource server!',
'config.json': JSON.stringify({ setting1: 'value1', setting2: 'value2' }, null, 2),
'data.txt': 'Sample text file content'
};
const content = simulatedFiles[path as string];
if (!content) {
return {
contents: [{
uri: uri.href,
mimeType: 'text/plain',
text: `File "${path}" not found. Available: ${Object.keys(simulatedFiles).join(', ')}`
}]
};
}
const mimeType = (path as string).endsWith('.json')
? 'application/json'
: (path as string).endsWith('.md')
? 'text/markdown'
: 'text/plain';
return {
contents: [{
uri: uri.href,
mimeType,
text: content
}]
};
}
);
// HTTP endpoint setup
const app = new Hono<{ Bindings: Env }>();
app.get('/', (c) => {
return c.json({
name: 'resource-server',
version: '1.0.0',
resources: [
'config://app',
'env://info',
'user://{userId}',
'data://{key}',
'users://all',
'file://{path}'
]
});
});
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
export default app;