Model Context Protocol (MCP) servers expose external services as tools that the AI agent can call. Genie Helper uses three MCP servers: Directus (data layer), Ollama (LLM inference), and Stagehand (browser automation).
MCP Server Architecture
AnythingLLM Agent
↓
storage/plugins/anythingllm_mcp_servers.json (config)
↓
scripts/
├── directus-mcp-server.mjs (17 tools)
├── ollama-mcp-server.mjs (3 tools)
└── stagehand-mcp-server.mjs (9 tools)
Servers are auto-booted on AnythingLLM startup via patched server/utils/boot/index.js.
MCP Server Configuration
Servers are defined in storage/plugins/anythingllm_mcp_servers.json:
{
"mcpServers" : {
"directus" : {
"command" : "node" ,
"args" : [ "/var/www/vhosts/geniehelper.com/agentx/scripts/directus-mcp-server.mjs" ],
"env" : {
"DIRECTUS_URL" : "http://127.0.0.1:8055" ,
"DIRECTUS_EMAIL" : "[email protected] " ,
"DIRECTUS_PASSWORD" : "password"
}
},
"ollama" : {
"command" : "node" ,
"args" : [ "/var/www/vhosts/geniehelper.com/agentx/scripts/ollama-mcp-server.mjs" ],
"env" : {
"OLLAMA_URL" : "http://127.0.0.1:11434" ,
"OLLAMA_MODEL" : "dolphin3:8b-llama3.1-q4_K_M"
}
},
"stagehand" : {
"command" : "node" ,
"args" : [ "/var/www/vhosts/geniehelper.com/agentx/scripts/stagehand-mcp-server.mjs" ],
"env" : {
"STAGEHAND_URL" : "http://127.0.0.1:3002" ,
"STAGEHAND_MODEL" : "ollama/qwen-2.5"
}
}
}
}
After modifying this file, restart AnythingLLM: pm2 restart anything-llm
Let’s add a new tool to the Directus MCP server.
File: scripts/directus-mcp-server.mjs
server . tool (
"bulk-delete-items" ,
"Delete multiple items from a Directus collection by ID array" ,
{
collection: z . string (). describe ( "Collection name" ),
ids: z . array ( z . string ()). describe ( "Array of item IDs to delete" ),
},
async ({ collection , ids }) => {
const promises = ids . map ( id =>
directusFetch ( `/items/ ${ collection } / ${ id } ` , { method: "DELETE" })
);
const results = await Promise . allSettled ( promises );
const deleted = results . filter ( r => r . status === "fulfilled" ). length ;
const failed = results . length - deleted ;
return {
content: [{
type: "text" ,
text: `Deleted ${ deleted } items. Failed: ${ failed } `
}]
};
}
);
server . tool (
"tool-name" , // Unique identifier (kebab-case)
"Human description" , // Shown to AI agent
{
// Zod schema for parameters
param1: z . string (). describe ( "What this param does" ),
param2: z . number (). optional (). describe ( "Optional param" ),
},
async ( params ) => {
// Implementation
return {
content: [{
type: "text" ,
text: "Result data as string or JSON"
}]
};
}
);
Key points:
Tool names use kebab-case
Descriptions help the AI understand when to use the tool
Zod schemas validate and document parameters
Always include .describe() for parameters
Return format: { content: [{ type: "text", text: "..." }] }
Creating a New MCP Server
Let’s create a new MCP server for Stripe payment integration.
1. Create Server Script
File: scripts/stripe-mcp-server.mjs
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
import Stripe from "stripe" ;
const STRIPE_SECRET_KEY = process . env . STRIPE_SECRET_KEY ;
const stripe = new Stripe ( STRIPE_SECRET_KEY );
const server = new McpServer ({ name: "stripe" , version: "1.0.0" });
server . tool (
"list-customers" ,
"List Stripe customers with optional email filter" ,
{
email: z . string (). optional (). describe ( "Filter by customer email" ),
limit: z . number (). optional (). describe ( "Max customers to return (default 10)" ),
},
async ({ email , limit }) => {
const params = { limit: limit || 10 };
if ( email ) params . email = email ;
const customers = await stripe . customers . list ( params );
return {
content: [{
type: "text" ,
text: JSON . stringify ( customers . data , null , 2 )
}]
};
}
);
server . tool (
"create-payment-link" ,
"Create a Stripe payment link for a subscription or one-time purchase" ,
{
price_id: z . string (). describe ( "Stripe Price ID" ),
quantity: z . number (). optional (). describe ( "Quantity (default 1)" ),
},
async ({ price_id , quantity }) => {
const paymentLink = await stripe . paymentLinks . create ({
line_items: [{ price: price_id , quantity: quantity || 1 }],
});
return {
content: [{
type: "text" ,
text: `Payment link created: ${ paymentLink . url } `
}]
};
}
);
const transport = new StdioServerTransport ();
await server . connect ( transport );
2. Make Script Executable
chmod +x scripts/stripe-mcp-server.mjs
3. Add to MCP Config
File: storage/plugins/anythingllm_mcp_servers.json
{
"mcpServers" : {
"directus" : { ... },
"ollama" : { ... },
"stagehand" : { ... },
"stripe" : {
"command" : "node" ,
"args" : [ "/var/www/vhosts/geniehelper.com/agentx/scripts/stripe-mcp-server.mjs" ],
"env" : {
"STRIPE_SECRET_KEY" : "sk_live_..."
}
}
}
}
4. Install Dependencies
5. Restart AnythingLLM
Check AnythingLLM logs:
pm2 logs anything-llm --lines 50 | grep "MCP server"
You should see:
MCP server 'stripe' loaded with 2 tools
MCP Server Templates
Basic HTTP API Server
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
const API_URL = process . env . API_URL || "https://api.example.com" ;
const API_KEY = process . env . API_KEY ;
async function apiFetch ( path , opts = {}) {
const res = await fetch ( ` ${ API_URL }${ path } ` , {
... opts ,
headers: {
"Authorization" : `Bearer ${ API_KEY } ` ,
"Content-Type" : "application/json" ,
... opts . headers ,
},
});
const text = await res . text ();
if ( ! res . ok ) throw new Error ( `API ${ path } failed ( ${ res . status } ): ${ text } ` );
try {
return JSON . parse ( text );
} catch {
return text ;
}
}
const server = new McpServer ({ name: "my-api" , version: "1.0.0" });
server . tool (
"get-data" ,
"Fetch data from the API" ,
{
id: z . string (). describe ( "Record ID" ),
},
async ({ id }) => {
const data = await apiFetch ( `/records/ ${ id } ` );
return { content: [{ type: "text" , text: JSON . stringify ( data , null , 2 ) }] };
}
);
const transport = new StdioServerTransport ();
await server . connect ( transport );
Database Connection Server
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
import mysql from "mysql2/promise" ;
const pool = mysql . createPool ({
host: process . env . DB_HOST || "localhost" ,
user: process . env . DB_USER ,
password: process . env . DB_PASSWORD ,
database: process . env . DB_NAME ,
});
const server = new McpServer ({ name: "mysql" , version: "1.0.0" });
server . tool (
"query" ,
"Execute a SQL query and return results" ,
{
sql: z . string (). describe ( "SQL query to execute" ),
params: z . array ( z . any ()). optional (). describe ( "Query parameters" ),
},
async ({ sql , params }) => {
const [ rows ] = await pool . execute ( sql , params || []);
return { content: [{ type: "text" , text: JSON . stringify ( rows , null , 2 ) }] };
}
);
const transport = new StdioServerTransport ();
await server . connect ( transport );
Collections:
list-collections — List all custom collections
get-collection-schema — Get field schema for a collection
Items CRUD:
read-items — Read items with filtering, sorting, pagination
read-item — Read single item by ID
create-item — Create new item
update-item — Update existing item
delete-item — Delete item
search-items — Full-text search
Flows:
trigger-flow — Trigger Directus Flow by UUID
list-flows — List all flows
Users:
get-me — Get current user info
list-users — List all users
get-user — Get user by ID
create-user — Create new user
update-user — Update user
Files:
list-files — List uploaded files
get-file — Get file metadata
See: scripts/directus-mcp-server.mjs
list-models — List locally available models
generate — Single-turn completion
chat — Multi-turn chat with history
See: scripts/ollama-mcp-server.mjs
start-session — Create browser session
navigate — Navigate to URL
act — Perform action (click, type, etc.)
extract — Extract data using AI
observe — Observe page elements
close-session — End browser session
set-cookies — Inject cookies
get-cookies — Retrieve cookies
screenshot — Capture screenshot
See: scripts/stagehand-mcp-server.mjs
Best Practices
Use kebab-case: get-user, create-payment-link
Be descriptive: bulk-delete-items not delete-many
Group by resource: user-create, user-update, user-delete
Parameter Descriptions
Always use .describe() for Zod schemas:
// Good
user_id : z . string (). describe ( "Directus user ID (UUID format)" )
// Bad
user_id : z . string ()
Descriptions are shown to the AI agent and improve tool selection accuracy.
Error Handling
server . tool (
"risky-operation" ,
"Operation that might fail" ,
{ id: z . string () },
async ({ id }) => {
try {
const result = await dangerousOperation ( id );
return { content: [{ type: "text" , text: JSON . stringify ( result ) }] };
} catch ( err ) {
return {
content: [{
type: "text" ,
text: `Error: ${ err . message } `
}]
};
}
}
);
Caching
For expensive operations, use caching:
const cache = new Map ();
const CACHE_TTL = 5 * 60 * 1000 ; // 5 minutes
server . tool (
"get-expensive-data" ,
"Fetch data with caching" ,
{ id: z . string () },
async ({ id }) => {
const cached = cache . get ( id );
if ( cached && Date . now () - cached . timestamp < CACHE_TTL ) {
return { content: [{ type: "text" , text: cached . data }] };
}
const data = await expensiveOperation ( id );
cache . set ( id , { data: JSON . stringify ( data ), timestamp: Date . now () });
return { content: [{ type: "text" , text: JSON . stringify ( data ) }] };
}
);
See: server/utils/cache/extractCache.js for cache implementation pattern.
Debugging MCP Servers
View Server Logs
pm2 logs anything-llm --lines 200 | grep MCP
MCP servers use stdio transport. Test with:
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list-models"},"id":1}' | \
node scripts/ollama-mcp-server.mjs
Common Issues
Tool not appearing in agent:
Check MCP config syntax in anythingllm_mcp_servers.json
Verify script path is absolute
Restart AnythingLLM: pm2 restart anything-llm
Environment variables not loaded:
Set in env block of MCP config, not in shell
Use absolute paths for any file references
Server crashes on startup:
Check pm2 logs anything-llm for stack trace
Verify all dependencies installed: npm install
Test script directly: node scripts/my-mcp-server.mjs
Auto-boot Implementation
MCP servers are automatically started by patched AnythingLLM boot sequence.
File: server/utils/boot/index.js
const { bootMCPServers } = require ( './mcpBoot' );
async function boot () {
// ... existing boot logic
// Boot MCP servers
await bootMCPServers ();
// ... rest of boot
}
This ensures MCP servers are available when the agent starts.
Next Steps
Custom Actions Use MCP tools in Action Runner flows
Project Structure Understand where MCP servers fit