Skip to content

MCP Server

MCP Server

Overview

MCP (Model Context Protocol) servers provide standardized interfaces for AI tools and resources within Raindrop applications. Each MCP server exposes tools, prompts, and resources that AI models can access through the standardized protocol.

MCP servers excel at creating reusable AI capabilities, exposing system functionality to language models, and building composable tool ecosystems. They provide type-safe interfaces with automatic validation and documentation generation.

Key benefits include standardized AI tool interfaces, automatic type validation, resource template support, and seamless integration with AI models.

Prerequisites

  • Active Raindrop project with manifest configuration
  • Understanding of TypeScript interfaces and callback patterns
  • Familiarity with AI tool concepts and prompt engineering
  • Knowledge of the Model Context Protocol specification

Creating/Getting Started

Define MCP servers in your manifest to expose AI-accessible capabilities:

application "ai-tools" {
mcp "file-manager" {
# Optional configuration
}
service "api" {
domain = "api.example.com"
}
}

Generate the MCP server implementation:

Terminal window
raindrop build generate

This creates typed environment bindings and base server structure:

interface Env {
FILE_MANAGER: RaindropMcpServer;
}

Accessing/Basic Usage

Use the MCP server binding to register tools and handle AI interactions:

export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
const mcpServer = this.env.FILE_MANAGER;
// Register a simple tool
mcpServer.tool("get-weather", "Get current weather for a location", async (args, extra) => {
const weather = await this.getWeather(args.location);
return {
content: [{
type: "text",
text: `Weather in ${args.location}: ${weather.description}, ${weather.temperature}°F`
}]
};
});
// Register tool with input validation
mcpServer.tool("calculate", "Perform mathematical calculations", {
num1: z.number(),
num2: z.number(),
operation: z.enum(["add", "subtract", "multiply", "divide"])
}, async (args, extra) => {
const result = this.calculate(args.num1, args.num2, args.operation);
return {
content: [{
type: "text",
text: `Result: ${result}`
}]
};
});
return new Response("MCP tools registered");
}
}

Connect the server to transport layer:

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
// Connect server to transport
const transport = new StdioServerTransport();
await mcpServer.connect(transport);

Core Concepts

MCP servers implement the Model Context Protocol to provide standardized interfaces for AI model interactions.

Tool Registration: Tools are functions that AI models can call with validated parameters and receive structured responses.

Resource Management: Resources represent accessible data or content that models can read through URI-based addressing.

Prompt Templates: Reusable prompt templates with parameter substitution for consistent AI interactions.

Type Safety: Full TypeScript integration with Zod schema validation for parameters and responses.

MCP Server Interface

The RaindropMcpServer interface provides comprehensive functionality for registering tools, prompts, and resources:

interface RaindropMcpServer {
// Connection management
connect(transport: Transport): Promise<void>;
close(): Promise<void>;
isConnected(): boolean;
// Tool registration - multiple overload signatures
tool(name: string, cb: ToolCallback): RegisteredTool;
tool(name: string, description: string, cb: ToolCallback): RegisteredTool;
tool<Args extends ZodRawShape>(name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, cb: ToolCallback<Args>): RegisteredTool;
tool<Args extends ZodRawShape>(name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, cb: ToolCallback<Args>): RegisteredTool;
tool<Args extends ZodRawShape>(name: string, paramsSchema: Args, annotations: ToolAnnotations, cb: ToolCallback<Args>): RegisteredTool;
tool<Args extends ZodRawShape>(name: string, description: string, paramsSchema: Args, annotations: ToolAnnotations, cb: ToolCallback<Args>): RegisteredTool;
// Advanced tool registration with full configuration
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
name: string,
config: {
title?: string;
description?: string;
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
},
cb: ToolCallback<InputArgs>
): RegisteredTool;
// Prompt registration - multiple overload signatures
prompt(name: string, cb: PromptCallback): RegisteredPrompt;
prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt;
prompt<Args extends PromptArgsRawShape>(name: string, argsSchema: Args, cb: PromptCallback<Args>): RegisteredPrompt;
prompt<Args extends PromptArgsRawShape>(name: string, description: string, argsSchema: Args, cb: PromptCallback<Args>): RegisteredPrompt;
// Advanced prompt registration with full configuration
registerPrompt<Args extends PromptArgsRawShape>(
name: string,
config: {
title?: string;
description?: string;
argsSchema?: Args;
},
cb: PromptCallback<Args>
): RegisteredPrompt;
// Resource registration - multiple overload signatures
resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource;
resource(name: string, uri: string, metadata: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource;
resource(name: string, template: ResourceTemplate, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate;
resource(name: string, template: ResourceTemplate, metadata: ResourceMetadata, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate;
// Advanced resource registration with full configuration
registerResource(
name: string,
uriOrTemplate: string | ResourceTemplate,
config: ResourceMetadata,
readCallback: ReadResourceCallback,
): RegisteredResource | RegisteredResourceTemplate;
// Notification methods for dynamic updates
sendToolListChanged(): void;
sendResourceListChanged(): void;
sendPromptListChanged(): void;
// Access to underlying MCP Server for advanced usage
readonly server: Server;
}

Callback Types

Type-safe callback signatures for different MCP operations with complete request context:

type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
Args extends ZodRawShape
? (args: z.objectOutputType<Args, ZodTypeAny>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
type PromptCallback<Args extends undefined | PromptArgsRawShape = undefined> =
Args extends PromptArgsRawShape
? (args: z.objectOutputType<Args, ZodTypeAny>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
// Resource callbacks with full context information
type ReadResourceCallback = (uri: URL, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ReadResourceResult | Promise<ReadResourceResult>;
type ReadResourceTemplateCallback = (uri: URL, variables: Variables, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ReadResourceResult | Promise<ReadResourceResult>;
// Additional helper types for resource management
type ResourceMetadata = Omit<Resource, "uri" | "name">;
type PromptArgsRawShape = {
[k: string]: ZodType<string, ZodTypeDef, string> | ZodOptional<ZodType<string, ZodTypeDef, string>>;
};

Tool Registration

Register AI-accessible tools with parameter validation and response formatting.

tool(name, callback)

Register a basic tool without parameters:

const weatherTool = mcpServer.tool("current-time", async (extra) => {
const now = new Date();
return {
content: [{
type: "text",
text: `Current time: ${now.toISOString()}`
}]
};
});
// Tool can be enabled/disabled
weatherTool.disable();
weatherTool.enable();

tool(name, description, callback)

Register a tool with description:

mcpServer.tool(
"generate-uuid",
"Generate a new UUID v4",
async (extra) => {
const uuid = crypto.randomUUID();
return {
content: [{
type: "text",
text: uuid
}]
};
}
);

tool(name, paramsSchema, callback)

Register a tool with parameter validation:

import { z } from 'zod';
mcpServer.tool("send-email", {
recipient: z.string().email(),
subject: z.string(),
body: z.string(),
priority: z.enum(["high", "normal", "low"]).optional()
}, async (args, extra) => {
// args is fully typed based on schema
const emailResult = await sendEmail({
to: args.recipient,
subject: args.subject,
body: args.body,
priority: args.priority || "normal"
});
return {
content: [{
type: "text",
text: `Email sent successfully. Message ID: ${emailResult.messageId}`
}]
};
});

Advanced Tool Registration

Use the registerTool method for full configuration:

const fileTool = mcpServer.registerTool("read-file", {
title: "File Reader",
description: "Read contents of a text file",
inputSchema: {
path: z.string().describe("File path to read"),
encoding: z.string().optional().describe("File encoding (default: utf-8)")
},
outputSchema: {
content: z.string(),
size: z.number(),
lastModified: z.date()
},
annotations: {
audience: ["user"],
dangerLevel: "low"
}
}, async (args, extra) => {
const fileContent = await readFile(args.path, args.encoding || 'utf-8');
const stats = await getFileStats(args.path);
return {
content: [{
type: "text",
text: fileContent
}],
metadata: {
size: stats.size,
lastModified: stats.mtime
}
};
});
// Update tool configuration
fileTool.update({
description: "Read and return the contents of any text file",
enabled: true
});
// Remove tool
fileTool.remove();

Resource Management

Expose data and content through URI-based resource interfaces that AI models can access.

resource(name, uri, callback)

Register a static resource with fixed URI:

mcpServer.resource("config", "file:///app/config.json", async (uri, extra) => {
const configPath = uri.pathname;
const config = await readFile(configPath, 'utf-8');
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: config
}]
};
});

resource(name, template, callback)

Register a resource template for dynamic URIs:

mcpServer.resource("user-profile", {
uriTemplate: "user://{userId}/profile",
name: "User Profile",
description: "Get user profile information"
}, async (uri, variables, extra) => {
const userId = variables.userId;
const userProfile = await getUserProfile(userId);
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(userProfile, null, 2)
}]
};
});

Resource with Metadata

Include additional metadata for resource discovery:

mcpServer.resource("database-schema", "schema://main", {
description: "Database schema information",
mimeType: "application/json",
tags: ["database", "schema", "metadata"]
}, async (uri, extra) => {
const schema = await getDatabaseSchema();
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(schema),
metadata: {
version: schema.version,
lastUpdated: schema.updatedAt
}
}]
};
});

Prompt Templates

Create reusable prompt templates with parameter substitution for consistent AI interactions.

prompt(name, callback)

Register a basic prompt without parameters:

mcpServer.prompt("system-info", async (extra) => {
const systemInfo = await getSystemInfo();
return {
messages: [{
role: "system",
content: {
type: "text",
text: `System Information:
- OS: ${systemInfo.os}
- Memory: ${systemInfo.memory}
- CPU: ${systemInfo.cpu}
- Uptime: ${systemInfo.uptime}
Use this information to help with system-related questions.`
}
}]
};
});

prompt(name, argsSchema, callback)

Register a parameterized prompt template:

mcpServer.prompt("code-review", {
language: z.string().describe("Programming language"),
code: z.string().describe("Code to review"),
focus: z.enum(["performance", "security", "readability", "bugs"]).optional()
}, async (args, extra) => {
const focusArea = args.focus || "general";
return {
messages: [{
role: "system",
content: {
type: "text",
text: `You are a senior software engineer performing a code review.
Language: ${args.language}
Focus Area: ${focusArea}
Please review the following code and provide constructive feedback:`
}
}, {
role: "user",
content: {
type: "text",
text: args.code
}
}]
};
});

Connection Management

Handle MCP server lifecycle and transport connections.

connect(transport)

Connect server to a transport layer:

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
// Connect to stdio transport
const stdioTransport = new StdioServerTransport();
await mcpServer.connect(stdioTransport);
// Connect to SSE transport
const sseTransport = new SSEServerTransport("/mcp", response);
await mcpServer.connect(sseTransport);

Connection Status

Check and manage connection status:

if (mcpServer.isConnected()) {
console.log("MCP server is connected");
// Send notifications about changes
mcpServer.sendToolListChanged();
mcpServer.sendResourceListChanged();
mcpServer.sendPromptListChanged();
}
// Gracefully close connection
await mcpServer.close();

Code Examples

Complete MCP server implementations demonstrating common patterns and use cases.

File Management MCP Server

// manifest.hcl
application "file-tools" {
mcp "file-manager" {}
service "mcp-service" {
domain = "files.example.com"
}
}
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
const mcpServer = this.env.FILE_MANAGER;
// Register file operations
this.registerFileTools(mcpServer);
this.registerFileResources(mcpServer);
this.registerFilePrompts(mcpServer);
return new Response("File management MCP server configured");
}
private registerFileTools(server: RaindropMcpServer): void {
// List directory contents
server.tool("list-directory", {
path: z.string().describe("Directory path to list"),
showHidden: z.boolean().optional().describe("Include hidden files")
}, async (args, extra) => {
const files = await fs.readdir(args.path, { withFileTypes: true });
const fileList = files
.filter(file => args.showHidden || !file.name.startsWith('.'))
.map(file => ({
name: file.name,
type: file.isDirectory() ? 'directory' : 'file',
path: path.join(args.path, file.name)
}));
return {
content: [{
type: "text",
text: JSON.stringify(fileList, null, 2)
}]
};
});
// Read file contents
server.tool("read-file", {
path: z.string().describe("File path to read"),
encoding: z.string().optional().default("utf-8").describe("File encoding")
}, async (args, extra) => {
try {
const content = await fs.readFile(args.path, args.encoding as BufferEncoding);
const stats = await fs.stat(args.path);
return {
content: [{
type: "text",
text: content.toString()
}],
metadata: {
size: stats.size,
lastModified: stats.mtime.toISOString()
}
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error reading file: ${error.message}`
}],
isError: true
};
}
});
// Write file contents
server.tool("write-file", {
path: z.string().describe("File path to write"),
content: z.string().describe("Content to write"),
createDirs: z.boolean().optional().describe("Create parent directories if needed")
}, async (args, extra) => {
try {
if (args.createDirs) {
await fs.mkdir(path.dirname(args.path), { recursive: true });
}
await fs.writeFile(args.path, args.content);
const stats = await fs.stat(args.path);
return {
content: [{
type: "text",
text: `File written successfully: ${args.path}`
}],
metadata: {
size: stats.size,
created: stats.birthtime.toISOString()
}
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error writing file: ${error.message}`
}],
isError: true
};
}
});
// Search files
server.tool("search-files", {
directory: z.string().describe("Directory to search in"),
pattern: z.string().describe("Search pattern (glob)"),
content: z.string().optional().describe("Search within file contents")
}, async (args, extra) => {
const results = await this.searchFiles(args.directory, args.pattern, args.content);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
});
}
private registerFileResources(server: RaindropMcpServer): void {
// Dynamic file resource
server.resource("file", {
uriTemplate: "file://{path}",
name: "File Resource",
description: "Access file contents by path"
}, async (uri, variables, extra) => {
const filePath = variables.path;
try {
const content = await fs.readFile(filePath, 'utf-8');
const stats = await fs.stat(filePath);
return {
contents: [{
uri: uri.toString(),
mimeType: this.getMimeType(filePath),
text: content,
metadata: {
size: stats.size,
lastModified: stats.mtime.toISOString()
}
}]
};
} catch (error) {
return {
contents: [{
uri: uri.toString(),
mimeType: "text/plain",
text: `Error accessing file: ${error.message}`
}]
};
}
});
// Directory listing resource
server.resource("directory", {
uriTemplate: "dir://{path}",
name: "Directory Listing",
description: "List directory contents"
}, async (uri, variables, extra) => {
const dirPath = variables.path;
try {
const files = await fs.readdir(dirPath, { withFileTypes: true });
const listing = files.map(file => ({
name: file.name,
type: file.isDirectory() ? 'directory' : 'file',
path: path.join(dirPath, file.name)
}));
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(listing, null, 2)
}]
};
} catch (error) {
return {
contents: [{
uri: uri.toString(),
mimeType: "text/plain",
text: `Error listing directory: ${error.message}`
}]
};
}
});
}
private registerFilePrompts(server: RaindropMcpServer): void {
// Code analysis prompt
server.prompt("analyze-code", {
filePath: z.string().describe("Path to code file"),
analysisType: z.enum(["structure", "complexity", "security", "performance"]).describe("Type of analysis")
}, async (args, extra) => {
const code = await fs.readFile(args.filePath, 'utf-8');
const fileExtension = path.extname(args.filePath);
return {
messages: [{
role: "system",
content: {
type: "text",
text: `You are a code analysis expert. Please analyze the following ${fileExtension} code file with focus on ${args.analysisType}.
File: ${args.filePath}
Provide detailed insights about:
${this.getAnalysisPrompt(args.analysisType)}`
}
}, {
role: "user",
content: {
type: "text",
text: code
}
}]
};
});
}
private async searchFiles(directory: string, pattern: string, contentSearch?: string): Promise<any[]> {
// Implementation would use glob pattern matching and content search
// This is a simplified version
const files = await fs.readdir(directory, { recursive: true, withFileTypes: true });
return files
.filter(file => file.isFile())
.filter(file => file.name.includes(pattern))
.map(file => ({
name: file.name,
path: path.join(directory, file.name)
}));
}
private getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
'.txt': 'text/plain',
'.json': 'application/json',
'.js': 'text/javascript',
'.ts': 'text/typescript',
'.py': 'text/x-python',
'.md': 'text/markdown'
};
return mimeTypes[ext] || 'text/plain';
}
private getAnalysisPrompt(type: string): string {
const prompts = {
structure: "- Code organization and architecture\n- Function and class design\n- Module dependencies",
complexity: "- Cyclomatic complexity\n- Code readability\n- Potential simplifications",
security: "- Security vulnerabilities\n- Input validation\n- Authentication and authorization",
performance: "- Performance bottlenecks\n- Memory usage\n- Optimization opportunities"
};
return prompts[type] || "General code quality assessment";
}
}

Database Query MCP Server

export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
const mcpServer = this.env.DATABASE_TOOLS;
// Query execution tool
mcpServer.tool("execute-query", {
sql: z.string().describe("SQL query to execute"),
params: z.array(z.any()).optional().describe("Query parameters"),
readonly: z.boolean().optional().default(true).describe("Execute as read-only")
}, async (args, extra) => {
if (!args.readonly && !args.sql.match(/^(SELECT|WITH)/i)) {
return {
content: [{
type: "text",
text: "Only SELECT queries allowed in read-only mode"
}],
isError: true
};
}
try {
const result = await this.env.DATABASE.prepare(args.sql).bind(...(args.params || [])).all();
return {
content: [{
type: "text",
text: JSON.stringify({
rows: result.results,
rowCount: result.results.length,
success: true
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Database error: ${error.message}`
}],
isError: true
};
}
});
// Schema inspection tool
mcpServer.tool("describe-table", {
tableName: z.string().describe("Name of table to describe")
}, async (args, extra) => {
const schema = await this.env.DATABASE.prepare(
"PRAGMA table_info(?)"
).bind(args.tableName).all();
return {
content: [{
type: "text",
text: JSON.stringify(schema.results, null, 2)
}]
};
});
return new Response("Database MCP server configured");
}
}
## raindrop.manifest
Configure MCP servers in your manifest to create AI-accessible tool providers:
```hcl
application "ai-platform" {
mcp "file-tools" {
# MCP server for file system operations
}
mcp "database-tools" {
# MCP server for database queries and operations
}
mcp "api-tools" {
# MCP server for external API integrations
}
service "mcp-host" {
domain = "tools.example.com"
# Service can access MCP servers via env.FILE_TOOLS, env.DATABASE_TOOLS, env.API_TOOLS
}
actor "tool-manager" {
# Actors can also register and manage MCP tools
}
}