Automated MCP Server Creation Workflow
<overview> This workflow creates a complete, working MCP server from scratch with zero manual configuration. Use this when Lex wants to build a new MCP server - it handles everything automatically.
Overview
Automated MCP Server Creation Workflow
<overview> This workflow creates a complete, working MCP server from scratch with zero manual configuration. Use this when Lex wants to build a new MCP server - it handles everything automatically.End state: Server running in both Claude Code and Claude Desktop with all credentials configured. </overview>
<workflow>Task Progress Checklist
Copy this and check off items as you complete them:
- [ ] Step 1: Gather requirements
- [ ] Step 2: Create project structure
- [ ] Step 3: Generate server code
- [ ] Step 4: Configure environment variables
- [ ] Step 5: Install in Claude Code
- [ ] Step 6: Install in Claude Desktop
- [ ] Step 7: Test and verify
Step 1: Gather Requirements
Use AskUserQuestion to collect all information upfront:
<questions>
<question>
<header>Server Name</header>
<question>What should this MCP server be called? (lowercase-with-hyphens)</question>
<options>
<option>
<label>Suggest based on purpose</label>
<description>I'll suggest a name after you describe what it does</description>
</option>
<option>
<label>I have a name</label>
<description>I know exactly what to call it</description>
</option>
</options>
<multiSelect>false</multiSelect>
</question>
<question>
<header>Language</header>
<question>Which language should I use?</question>
<options>
<option>
<label>Python</label>
<description>Recommended for API integrations, data processing, most use cases</description>
</option>
<option>
<label>TypeScript</label>
<description>Better for Node.js integrations, when you need strict typing</description>
</option>
</options>
<multiSelect>false</multiSelect>
</question>
<question>
<header>Purpose</header>
<question>What should this server do? What capabilities will it provide?</question>
<options>
<option>
<label>API Integration</label>
<description>Connect to external APIs (Stripe, Airtable, etc.)</description>
</option>
<option>
<label>File Operations</label>
<description>Read, write, process files on the filesystem</description>
</option>
<option>
<label>Database Access</label>
<description>Query and manage database records</description>
</option>
<option>
<label>Custom Tools</label>
<description>Specialized functions/calculations</description>
</option>
</options>
<multiSelect>true</multiSelect>
</question>
<question>
<header>Credentials</header>
<question>What environment variables/API keys does this server need?</question>
<options>
<option>
<label>API Keys</label>
<description>External service API keys</description>
</option>
<option>
<label>Database URL</label>
<description>Database connection string</description>
</option>
<option>
<label>None</label>
<description>No credentials needed</description>
</option>
</options>
<multiSelect>true</multiSelect>
</question>
</questions>
After gathering requirements:
- If server name wasn't provided, suggest one based on purpose
- Confirm the name:
{purpose}-mcp(e.g., "stripe-mcp", "notion-mcp") - List out all environment variables that will be needed
- Determine architecture based on operation count:
- 1-2 operations: Traditional architecture (flat tools)
- 3+ operations: On-demand discovery architecture (meta-tools + resources)
- Explain: "I'll use on-demand discovery to minimize context usage - this means only loading operation schemas when needed instead of all upfront."
Step 2: Create Project Structure
Execute these commands to set up the project:
For Python:
# Create directory
mkdir -p ~/Developer/mcp/{server-name}/src
cd ~/Developer/mcp/{server-name}
# Initialize project
uv init
# Add dependencies
uv add mcp
# If API integration, add requests
uv add httpx
# Create .gitignore
cat > .gitignore << 'EOF'
.env
.venv/
__pycache__/
*.pyc
.DS_Store
EOF
# Create README template
cat > README.md << 'EOF'
# {Server Name} MCP Server
## Description
{What this server does}
## Setup
1. Install: `uv sync`
2. Configure environment variables in ~/.zshrc
3. Run: `uv run python -m src.server`
## Environment Variables
{List of required env vars}
EOF
For TypeScript:
# Create directory
mkdir -p ~/Developer/mcp/{server-name}/src
cd ~/Developer/mcp/{server-name}
# Initialize npm project
npm init -y
# Add dependencies
npm install @modelcontextprotocol/sdk
# If API integration
npm install axios
# Create tsconfig.json
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
EOF
# Create .gitignore
cat > .gitignore << 'EOF'
.env
node_modules/
build/
.DS_Store
EOF
# Create README
cat > README.md << 'EOF'
# {Server Name} MCP Server
## Description
{What this server does}
## Setup
1. Install: `npm install`
2. Build: `npm run build`
3. Configure environment variables in ~/.zshrc
4. Run: `node build/index.js`
## Environment Variables
{List of required env vars}
EOF
Verify structure created:
ls -la ~/Developer/mcp/{server-name}/
Step 3: Generate Server Code
Write the server implementation based on requirements and chosen architecture.
Architecture Decision
If 1-2 operations: Use traditional template below If 3+ operations: Use on-demand discovery template (see references/large-api-pattern.md for complete implementation)
Traditional Architecture Template (1-2 Operations)
Python Template (API Integration):
# src/server.py
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Configuration
API_KEY = os.getenv("{ENV_VAR_NAME}")
if not API_KEY:
print("ERROR: {ENV_VAR_NAME} environment variable not set", file=sys.stderr)
sys.exit(1)
BASE_URL = "{api_base_url}"
app = Server("{server-name}")
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [
Tool(
name="{tool_name}",
description="{What this tool does}",
inputSchema={
"type": "object",
"properties": {
"{param_name}": {
"type": "string",
"description": "{Parameter description}"
}
},
"required": ["{param_name}"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Execute a tool."""
try:
if name == "{tool_name}":
param = arguments["{param_name}"]
# Make API request
async with httpx.AsyncClient() as client:
response = await client.get(
f"{BASE_URL}/{endpoint}",
headers={"Authorization": f"Bearer {API_KEY}"},
params={"param": param}
)
response.raise_for_status()
data = response.json()
return [TextContent(
type="text",
text=f"Result: {data}"
)]
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
print(f"Error in {name}: {e}", file=sys.stderr)
return [TextContent(
type="text",
text=f"Error: {str(e)}"
)]
async def main():
"""Run the MCP server."""
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
TypeScript Template (API Integration):
// src/index.ts
const API_KEY = process.env.{ENV_VAR_NAME};
if (!API_KEY) {
console.error("ERROR: {ENV_VAR_NAME} environment variable not set");
process.exit(1);
}
const BASE_URL = "{api_base_url}";
const server = new Server(
{ name: "{server-name}", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "{tool_name}",
description: "{What this tool does}",
inputSchema: {
type: "object",
properties: {
{param_name}: {
type: "string",
description: "{Parameter description}"
}
},
required: ["{param_name}"]
}
}
]
}));
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "{tool_name}") {
const param = args.{param_name};
const response = await axios.get(`${BASE_URL}/{endpoint}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
params: { param }
});
return {
content: [
{
type: "text",
text: `Result: ${JSON.stringify(response.data)}`
}
]
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
console.error(`Error in ${name}:`, error);
return {
content: [
{
type: "text",
text: `Error: ${error.message}`
}
]
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Customize the template:
- Replace
{server-name},{tool_name},{ENV_VAR_NAME}with actual values - Add multiple tools if needed
- Implement specific API logic based on requirements
- Add proper error handling for the specific API
On-Demand Discovery Architecture (3+ Operations)
Why: Minimizes context usage by loading operation schemas only when needed.
Implementation: Follow the complete guide in references/large-api-pattern.md which includes:
-
4 Meta-Tools Pattern:
discover- Browse available operationsget_schema- Get parameters for one operationexecute- Run an operationcontinue- Handle pagination
-
Operations JSON File: All operation definitions in
operations.json(not hardcoded in Python) -
MCP Resources: Operations exposed as resources with URIs like
{server}://operations/{category}/{action} -
Smart Dispatch: Maps operation strings to actual implementations
Quick reference for on-demand architecture:
# The 4 meta-tools handle everything
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(name="discover", ...),
Tool(name="get_schema", ...),
Tool(name="execute", ...),
Tool(name="continue", ...)
]
# Operations stored as MCP resources
@app.list_resources()
async def list_resources() -> list[Resource]:
# Load from operations.json
# Expose as resources
See large-api-pattern.md for:
- Complete Python implementation
- TypeScript implementation
- Operations JSON schema
- Dispatch layer implementation
- Testing and debugging
Write the Generated Code
# For Python
cat > ~/Developer/mcp/{server-name}/src/server.py << 'EOF'
{generated code}
EOF
# For TypeScript (then build)
cat > ~/Developer/mcp/{server-name}/src/index.ts << 'EOF'
{generated code}
EOF
npm run build
Step 4: Configure Environment Variables
SECURITY CRITICAL: NEVER ask Lex to paste secrets into chat. Secrets must never go through Anthropic's servers or appear in conversation history.
Provide Exact Commands
For each required environment variable, give Lex the exact commands to run in his terminal.
Step 4.1: Show required variables and where to get them
Present to Lex:
š Required Environment Variables:
{ENV_VAR_NAME_1} - {Description}
Get it from: {URL or instructions}
{ENV_VAR_NAME_2} - {Description}
Get it from: {URL or instructions}
Step 4.2: Give exact commands to add to ~/.zshrc
Run these commands in your terminal:
# Add {Server Name} credentials
cat >> ~/.zshrc << 'EOF'
# {Server Name} MCP Server
EOF
# Reload shell
source ~/.zshrc
Step 4.3: Wait for confirmation
Ask using AskUserQuestion:
<question>
<header>Environment Setup</header>
<question>Have you added the environment variables to ~/.zshrc?</question>
<options>
<option>
<label>Yes, added and sourced</label>
<description>Variables are ready</description>
</option>
<option>
<label>Skip for now</label>
<description>I'll add them later</description>
</option>
</options>
<multiSelect>false</multiSelect>
</question>
Step 4.4: Verify variables exist (without showing values)
# Check each variable is set (without printing values)
for var in {ENV_VAR_1} {ENV_VAR_2}; do
if [ -z "${!var}" ]; then
echo "ā $var not set - please add to ~/.zshrc and run: source ~/.zshrc"
else
echo "ā $var is set"
fi
done
Important notes:
- Variables are checked for existence only (not values)
- Values never appear in conversation or output
- If variables aren't set, stop and wait for Lex to add them
Step 5: Install in Claude Code
# Get absolute path to uv (for Python) or node (for TypeScript)
UV_PATH=$(which uv)
NODE_PATH=$(which node)
# Build environment flags
ENV_FLAGS=""
for var in {ENV_VAR1} {ENV_VAR2}; do
ENV_FLAGS+="--env $var=\\${$var} "
done
# Install based on language
if [ "{language}" = "Python" ]; then
claude mcp add --transport stdio {server-name} \\
$ENV_FLAGS \\
-- uv --directory ~/Developer/mcp/{server-name} run python -m src.server
else
claude mcp add --transport stdio {server-name} \\
$ENV_FLAGS \\
-- node ~/Developer/mcp/{server-name}/build/index.js
fi
# Verify installation
claude mcp list | grep {server-name}
Expected output:
{server-name}: ... - ā Connected
Step 6: Install in Claude Desktop
# Get paths
UV_PATH=$(which uv)
NODE_PATH=$(which node)
DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
# Backup config
cp "$DESKTOP_CONFIG" "$DESKTOP_CONFIG.backup.$(date +%s)"
# Create server config based on language
if [ "{language}" = "Python" ]; then
SERVER_CONFIG=$(cat <<EOF
{
"command": "$UV_PATH",
"args": ["--