Skip to content

MCP Service

This content is for the 0.6.2 version. Switch to the latest version for up-to-date documentation.

MCP Service

Overview

MCP (Model Context Protocol) services provide standardized interfaces between AI models and application functionality. Each MCP service exposes tools, prompts, and resources that AI models can access through the Model Context Protocol specification.

MCP services 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 seamless integration with AI models.

Key benefits include standardized AI tool interfaces, automatic parameter validation, resource template support, and direct integration with the Model Context Protocol ecosystem.

Prerequisites

  • Active Raindrop project with manifest configuration
  • Understanding of TypeScript interfaces and async callback patterns
  • Familiarity with AI tool concepts and the Model Context Protocol
  • Knowledge of Zod schema validation for parameter typing

Creating/Getting Started

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

application "ai-tools" {
mcp_service "tools" {
visibility = "protected"
authorization_server = "https://authkit.liquidmetal.run"
}
}

The service generates typed environment bindings for accessing the MCP server:

interface Env {
TOOLS: RaindropMcpServer;
}

Accessing/Basic Usage

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

import { ActorState } from "@liquidmetal-ai/raindrop-framework";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export default async (server: McpServer, env: Env, state: ActorState) => {
// Register a simple tool without parameters
server.tool("current-time", "Get the current timestamp", async (extra) => {
return {
content: [{
type: "text",
text: `Current time: ${new Date().toISOString()}`
}]
};
});
// Register tool with parameter validation
server.tool("calculate", "Perform mathematical calculations", {
num1: z.number().describe("First number"),
num2: z.number().describe("Second number"),
operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("Mathematical operation")
}, async (args, extra) => {
let result: number;
switch (args.operation) {
case "add": result = args.num1 + args.num2; break;
case "subtract": result = args.num1 - args.num2; break;
case "multiply": result = args.num1 * args.num2; break;
case "divide": result = args.num1 / args.num2; break;
}
return {
content: [{
type: "text",
text: `${args.num1} ${args.operation} ${args.num2} = ${result}`
}]
};
});
};

Core Concepts

MCP services implement the Model Context Protocol to provide standardized interfaces for AI model interactions through three main components:

Tool Registration: Tools are functions that AI models can call with validated parameters and receive structured responses. Each tool defines its input schema and returns formatted content.

Resource Management: Resources represent accessible data or content that models can read through URI-based addressing. Resources support both static URIs and dynamic templates.

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

Type Safety: Full TypeScript integration with Zod schema validation ensures type safety for parameters, responses, and all MCP interactions.

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 methods
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;
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 methods
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;
registerPrompt<Args extends PromptArgsRawShape>(
name: string,
config: {
title?: string;
description?: string;
argsSchema?: Args;
},
cb: PromptCallback<Args>
): RegisteredPrompt;
// Resource registration methods
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;
// Notification methods for dynamic updates
sendToolListChanged(): void;
sendResourceListChanged(): void;
sendPromptListChanged(): void;
// Access to underlying MCP Server
readonly server: Server;
}

Callback Type Signatures

Type-safe callback signatures provide 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>;
type ReadResourceCallback = (uri: URL, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ReadResourceResult | Promise<ReadResourceResult>;
type ReadResourceTemplateCallback = (uri: URL, variables: Variables, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ReadResourceResult | Promise<ReadResourceResult>;

Tool Registration

Register AI-accessible tools with parameter validation and response formatting through multiple method signatures.

tool(name, callback)

Register a basic tool without parameters:

const timeTool = server.tool("current-time", async (extra) => {
const now = new Date();
return {
content: [{
type: "text",
text: `Current time: ${now.toISOString()}`
}]
};
});
// Tools can be managed after registration
timeTool.disable();
timeTool.enable();

tool(name, description, callback)

Register a tool with description:

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

tool(name, paramsSchema, callback)

Register a tool with parameter validation:

server.tool("send-email", {
recipient: z.string().email().describe("Email recipient address"),
subject: z.string().describe("Email subject line"),
body: z.string().describe("Email body content"),
priority: z.enum(["high", "normal", "low"]).optional().describe("Message priority")
}, async (args, extra) => {
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}`
}]
};
});

registerTool - Advanced Registration

Use registerTool for full configuration control:

const apiTool = server.registerTool("fetch-data", {
title: "Data Fetcher",
description: "Fetch data from external API",
inputSchema: {
url: z.string().url().describe("API endpoint URL"),
method: z.enum(["GET", "POST"]).optional().describe("HTTP method")
},
outputSchema: {
data: z.any(),
status: z.number(),
headers: z.record(z.string())
},
annotations: {
audience: ["user"],
dangerLevel: "low"
}
}, async (args, extra) => {
const response = await fetch(args.url, {
method: args.method || "GET"
});
const data = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}],
metadata: {
status: response.status,
headers: Object.fromEntries(response.headers.entries())
}
};
});
// Update tool configuration
apiTool.update({
description: "Fetch and return data from any REST API endpoint",
enabled: true
});
// Remove tool
apiTool.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:

server.resource("api-status", "status://health", async (uri, extra) => {
const healthCheck = await checkSystemHealth();
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(healthCheck, null, 2)
}]
};
});

resource(name, template, callback)

Register a resource template for dynamic URIs:

server.resource("user-profile", {
uriTemplate: "user://{userId}/profile",
name: "User Profile",
description: "Access user profile information by ID"
}, 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 and management:

server.resource("api-docs", "docs://api/v1", {
description: "API documentation and schema information",
mimeType: "application/json",
tags: ["api", "documentation", "schema"]
}, async (uri, extra) => {
const apiDocs = await getApiDocumentation();
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(apiDocs, null, 2),
metadata: {
version: apiDocs.version,
lastUpdated: apiDocs.updatedAt
}
}]
};
});

Prompt Templates

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

prompt(name, callback)

Register a basic prompt without parameters:

server.prompt("system-info", async (extra) => {
const systemInfo = await getSystemInfo();
return {
messages: [{
role: "system",
content: {
type: "text",
text: `System Information:
- Environment: ${systemInfo.environment}
- Region: ${systemInfo.region}
- Version: ${systemInfo.version}
Use this information to help with system-related questions.`
}
}]
};
});

prompt(name, argsSchema, callback)

Register a parameterized prompt template:

server.prompt("code-review", {
language: z.string().describe("Programming language"),
code: z.string().describe("Code to review"),
focus: z.enum(["performance", "security", "readability", "bugs"]).optional().describe("Review focus area")
}, 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
}
}]
};
});

Code Examples

Complete MCP service implementations demonstrating common patterns and real-world use cases.

Smart Memory MCP Service

Based on the monet package implementation pattern:

// raindrop.manifest
application "smart-tools" {
mcp_service "smart-tools" {
visibility = "protected"
authorization_server = "https://authkit.liquidmetal.run"
domain {
cname = "smart-tools"
}
}
smartmemory "app_memory" {}
}
import { ActorState } from "@liquidmetal-ai/raindrop-framework";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Register smart memory tools
function registerSmartMemoryTools(server: McpServer, env: Env, state: ActorState) {
// Store information in smart memory
server.tool("store-memory", {
content: z.string().describe("Content to store"),
tags: z.array(z.string()).optional().describe("Tags for categorization")
}, async (args, extra) => {
const memory = env.APP_MEMORY;
const result = await memory.store(args.content, {
tags: args.tags || [],
timestamp: new Date().toISOString()
});
return {
content: [{
type: "text",
text: `Stored memory with ID: ${result.id}`
}]
};
});
// Search smart memory
server.tool("search-memory", {
query: z.string().describe("Search query"),
limit: z.number().optional().describe("Maximum results to return")
}, async (args, extra) => {
const memory = env.APP_MEMORY;
const results = await memory.search(args.query, {
limit: args.limit || 10
});
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
});
}
// Register documentation resources
async function registerDocumentationResources(server: McpServer, env: Env, state: ActorState) {
server.resource("documentation", {
uriTemplate: "docs://{docId}",
name: "Documentation",
description: "Access documentation by document ID"
}, async (uri, variables, extra) => {
const docId = variables.docId;
const doc = await getDocumentation(docId);
return {
contents: [{
uri: uri.toString(),
mimeType: "text/markdown",
text: doc.content
}]
};
});
}
export default async (server: McpServer, env: Env, state: ActorState) => {
registerSmartMemoryTools(server, env, state);
await registerDocumentationResources(server, env, state);
};

API Integration MCP Service

Complete API operations with error handling and response formatting:

import { z } from "zod";
export default async (server: McpServer, env: Env, state: ActorState) => {
// Make HTTP requests
server.tool("http-request", {
url: z.string().url().describe("Request URL"),
method: z.enum(["GET", "POST", "PUT", "DELETE"]).optional().describe("HTTP method"),
headers: z.record(z.string()).optional().describe("Request headers"),
body: z.string().optional().describe("Request body")
}, async (args, extra) => {
try {
const response = await fetch(args.url, {
method: args.method || "GET",
headers: args.headers,
body: args.body
});
const responseText = await response.text();
return {
content: [{
type: "text",
text: responseText
}],
metadata: {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
}
};
} catch (error) {
return {
content: [{
type: "text",
text: `Request failed: ${error.message}`
}],
isError: true
};
}
});
// Parse JSON data
server.tool("parse-json", {
json: z.string().describe("JSON string to parse"),
path: z.string().optional().describe("JSONPath expression to extract specific data")
}, async (args, extra) => {
try {
const data = JSON.parse(args.json);
if (args.path) {
// Simple JSONPath implementation for demonstration
const result = extractJsonPath(data, args.path);
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `JSON parsing error: ${error.message}`
}],
isError: true
};
}
});
// Format data as table
server.tool("format-table", {
data: z.array(z.record(z.any())).describe("Array of objects to format as table"),
columns: z.array(z.string()).optional().describe("Columns to include")
}, async (args, extra) => {
if (args.data.length === 0) {
return {
content: [{
type: "text",
text: "No data to format"
}]
};
}
const columns = args.columns || Object.keys(args.data[0]);
const table = formatAsTable(args.data, columns);
return {
content: [{
type: "text",
text: table
}]
};
});
// API status resource
server.resource("api-status", "status://api", async (uri, extra) => {
const status = await checkApiStatus();
return {
contents: [{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(status, null, 2)
}]
};
});
};
function extractJsonPath(data: any, path: string): any {
// Simplified JSONPath implementation
const parts = path.split('.');
let result = data;
for (const part of parts) {
if (result && typeof result === 'object' && part in result) {
result = result[part];
} else {
return undefined;
}
}
return result;
}
function formatAsTable(data: any[], columns: string[]): string {
const rows = data.map(row =>
columns.map(col => String(row[col] || '')).join('\t')
);
const header = columns.join('\t');
return [header, ...rows].join('\n');
}
async function checkApiStatus(): Promise<any> {
return {
status: "healthy",
timestamp: new Date().toISOString(),
services: {
database: "connected",
cache: "connected",
external_api: "connected"
}
};
}

raindrop.manifest

Configure MCP services in your manifest to create AI-accessible tool providers with secure authorization:

application "ai-platform" {
mcp_service "api-tools" {
visibility = "protected"
authorization_server = "https://authkit.liquidmetal.run"
domain {
cname = "api-tools"
}
}
mcp_service "data-tools" {
visibility = "protected"
authorization_server = "https://authkit.liquidmetal.run"
domain {
cname = "data-tools"
}
}
smartmemory "app_memory" {}
env "API_KEY" {
secret = true
}
}