MCP Server Starter
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP Server StarterAdd 5 and 3"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
π MCP Server Starter
Build your own AI tools in minutes!
This is a starter template for building MCP (Model Context Protocol) servers. MCP lets you create tools that AI assistants like Claude, VS Code Copilot, and other AI apps can use.
π Table of Contents
Related MCP server: MCP Server Boilerplate
π€ What is MCP?
Model Context Protocol (MCP) is an open protocol that lets AI assistants interact with external tools and data. Think of it as a USB port for AIβa standard way to plug in new capabilities.
How It Works
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β AI Assistant β MCP β Your Server β β External APIs β
β (Claude, etc) ββββββββββΊβ (This repo!) ββββββββββΊβ Databases, etc β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββAI assistant connects to your MCP server
Your server tells the AI what tools are available
AI decides when to use your tools based on user requests
Your tools execute and return results to the AI
What Can MCP Servers Provide?
Feature | Description | Example |
π§ Tools | Functions the AI can execute | Calculate, search, API calls |
π Resources | Data the AI can read | Files, configs, documentation |
π¬ Prompts | Pre-built prompt templates | Code review, debugging help |
β‘ Quick Start (2 minutes)
Prerequisites
Node.js 18+ - Download here
npm - Comes with Node.js
Installation
# 1. Clone this repository
git clone https://github.com/your-username/mcp-server-starter.git
cd mcp-server-starter
# 2. Install dependencies
npm install
# 3. Build the project
npm run build
# 4. Test your server!
npm run inspectorThe inspector opens a web UI at http://localhost:5173 where you can interactively test all your tools.
Verify It's Working
In the inspector:
Click on "Tools" in the sidebar
Select
addfrom the tool listEnter values for
aandbClick "Run" - you should see the result!
π Project Structure
mcp-server-starter/
βββ src/
β βββ index.ts # π Main entry point - starts the server
β βββ tools/
β β βββ _template.ts # π COPY THIS to create new tools!
β β βββ calculator.ts # β Example: math operations
β β βββ greeting.ts # π Example: greeting & utility tools
β βββ resources/
β β βββ index.ts # π Data sources AI can read
β βββ prompts/
β βββ index.ts # π¬ Pre-built prompt templates
βββ build/ # π¦ Compiled JavaScript (generated)
βββ package.json # π Dependencies and scripts
βββ tsconfig.json # βοΈ TypeScript configuration
βββ README.md # π You are here!Key Files Explained
File | Purpose |
| Creates the MCP server, imports all tools/resources/prompts, starts listening |
| Copy this file to create new tools - has examples and comments |
| Example showing basic math tools with error handling |
| Example showing string tools, enums, optional params |
| Example showing how to expose data to AI |
| Example showing reusable prompt templates |
π§ Core Concepts
1. Tools
Tools are functions that AI can execute. They have:
Name: Unique identifier (e.g.,
search_files)Description: Explains what the tool does (AI reads this!)
Parameters: Input schema validated with Zod
Handler: Async function that does the work
server.tool(
"tool_name", // Name (lowercase_with_underscores)
"What this tool does", // Description (be specific!)
{ /* parameters */ }, // Zod schema for inputs
async (params) => { // Handler function
// Do work here
return { content: [{ type: "text", text: "result" }] };
}
);2. Resources
Resources are data sources that AI can read. Unlike tools, they don't perform actionsβthey just provide information.
server.resource(
"mcp://server/config", // URI (unique identifier)
"Application configuration", // Description
{ mimeType: "application/json" },
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify(config) }]
})
);3. Prompts
Prompts are reusable templates that structure AI interactions.
server.prompt(
"code-review", // Name
"Review code for issues", // Description
{ code: z.string() }, // Arguments
async ({ code }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Review this code:\n${code}` }
}]
})
);π οΈ Adding Your First Tool
Step 1: Create a new file
cp src/tools/_template.ts src/tools/my-tools.tsStep 2: Write your tool
Edit src/tools/my-tools.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerMyTools(server: McpServer): void {
// A simple tool
server.tool(
"hello_world",
"Say hello to someone by name",
{
name: z.string().describe("The person's name to greet"),
},
async ({ name }) => {
return {
content: [{
type: "text",
text: `Hello, ${name}! Welcome to MCP!`
}],
};
}
);
// Add more tools here...
}Step 3: Register your tools
Edit src/index.ts and add:
// Add this import at the top
import { registerMyTools } from "./tools/my-tools.js";
// Add this line after other registrations (before server.connect)
registerMyTools(server);Step 4: Build and test
npm run build
npm run inspectorYour hello_world tool should now appear in the inspector!
π Tool Examples Cookbook
π€ String Operations
// Reverse a string
server.tool(
"reverse_string",
"Reverse the characters in a string",
{ text: z.string().describe("Text to reverse") },
async ({ text }) => ({
content: [{ type: "text", text: text.split("").reverse().join("") }],
})
);
// Count words
server.tool(
"count_words",
"Count the number of words in text",
{ text: z.string().describe("Text to analyze") },
async ({ text }) => {
const count = text.trim().split(/\s+/).filter(w => w.length > 0).length;
return { content: [{ type: "text", text: `Word count: ${count}` }] };
}
);π’ Math Operations
// Calculate percentage
server.tool(
"calculate_percentage",
"Calculate what percentage A is of B",
{
part: z.number().describe("The part value"),
whole: z.number().describe("The whole value"),
},
async ({ part, whole }) => {
if (whole === 0) {
return { content: [{ type: "text", text: "Error: Cannot divide by zero" }], isError: true };
}
const percentage = (part / whole) * 100;
return { content: [{ type: "text", text: `${percentage.toFixed(2)}%` }] };
}
);π Working with Enums
// Format text in different styles
server.tool(
"format_text",
"Format text in various styles",
{
text: z.string().describe("Text to format"),
style: z.enum(["uppercase", "lowercase", "title", "sentence"])
.describe("Formatting style to apply"),
},
async ({ text, style }) => {
let result: string;
switch (style) {
case "uppercase": result = text.toUpperCase(); break;
case "lowercase": result = text.toLowerCase(); break;
case "title": result = text.replace(/\w\S*/g, t =>
t.charAt(0).toUpperCase() + t.slice(1).toLowerCase()
); break;
case "sentence": result = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); break;
}
return { content: [{ type: "text", text: result }] };
}
);β Optional Parameters
// Greet with optional title
server.tool(
"formal_greeting",
"Generate a formal greeting",
{
name: z.string().describe("Person's name"),
title: z.enum(["Mr", "Ms", "Mrs", "Dr", "Prof"]).optional()
.describe("Optional title/honorific"),
includeTime: z.boolean().optional().default(false)
.describe("Include time-based greeting"),
},
async ({ name, title, includeTime }) => {
const fullName = title ? `${title}. ${name}` : name;
let greeting = `Hello, ${fullName}`;
if (includeTime) {
const hour = new Date().getHours();
const timeGreeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
greeting = `${timeGreeting}, ${fullName}`;
}
return { content: [{ type: "text", text: greeting }] };
}
);π API Calls
// Fetch data from an API
server.tool(
"get_github_user",
"Get information about a GitHub user",
{ username: z.string().describe("GitHub username") },
async ({ username }) => {
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
return {
content: [{ type: "text", text: `User '${username}' not found` }],
isError: true
};
}
const user = await response.json();
const info = [
`Name: ${user.name || 'N/A'}`,
`Bio: ${user.bio || 'N/A'}`,
`Public Repos: ${user.public_repos}`,
`Followers: ${user.followers}`,
].join('\n');
return { content: [{ type: "text", text: info }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true
};
}
}
);π File System Operations
import * as fs from "fs/promises";
import * as path from "path";
// Read a file
server.tool(
"read_file",
"Read contents of a file",
{
filePath: z.string().describe("Path to the file to read"),
encoding: z.enum(["utf8", "base64"]).optional().default("utf8"),
},
async ({ filePath, encoding }) => {
try {
const content = await fs.readFile(filePath, encoding as BufferEncoding);
return { content: [{ type: "text", text: content }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error reading file: ${error.message}` }],
isError: true
};
}
}
);
// List directory contents
server.tool(
"list_directory",
"List files and folders in a directory",
{ dirPath: z.string().describe("Path to the directory") },
async ({ dirPath }) => {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const listing = entries.map(e =>
`${e.isDirectory() ? 'π' : 'π'} ${e.name}`
).join('\n');
return { content: [{ type: "text", text: listing }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true
};
}
}
);ποΈ Database Operations
// Example with a hypothetical database
server.tool(
"query_users",
"Search for users in the database",
{
searchTerm: z.string().describe("Name or email to search for"),
limit: z.number().min(1).max(100).optional().default(10)
.describe("Maximum number of results"),
},
async ({ searchTerm, limit }) => {
try {
// Replace with your actual database query
const results = await db.query(
"SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?",
[`%${searchTerm}%`, `%${searchTerm}%`, limit]
);
if (results.length === 0) {
return { content: [{ type: "text", text: "No users found" }] };
}
const formatted = results.map(u => `- ${u.name} (${u.email})`).join('\n');
return { content: [{ type: "text", text: `Found ${results.length} users:\n${formatted}` }] };
} catch (error) {
return {
content: [{ type: "text", text: `Database error: ${error.message}` }],
isError: true
};
}
}
);β οΈ Comprehensive Error Handling
server.tool(
"safe_divide",
"Safely divide two numbers with comprehensive error handling",
{
dividend: z.number().describe("Number to be divided"),
divisor: z.number().describe("Number to divide by"),
},
async ({ dividend, divisor }) => {
// Validate inputs
if (!Number.isFinite(dividend) || !Number.isFinite(divisor)) {
return {
content: [{ type: "text", text: "Error: Both inputs must be finite numbers" }],
isError: true,
};
}
// Check for division by zero
if (divisor === 0) {
return {
content: [{ type: "text", text: "Error: Cannot divide by zero" }],
isError: true,
};
}
// Perform calculation
const result = dividend / divisor;
// Check for overflow
if (!Number.isFinite(result)) {
return {
content: [{ type: "text", text: "Error: Result is too large" }],
isError: true,
};
}
return {
content: [{
type: "text",
text: `${dividend} Γ· ${divisor} = ${result}`
}],
};
}
);π Adding Resources
Resources let AI read data from your server. Add them in src/resources/index.ts:
// Static text resource
server.resource(
"mcp://server/readme",
"Project README file",
{ mimeType: "text/plain" },
async (uri) => ({
contents: [{
uri: uri.href,
text: "This is the readme content..."
}]
})
);
// Dynamic JSON resource
server.resource(
"mcp://server/status",
"Current server status",
{ mimeType: "application/json" },
async (uri) => ({
contents: [{
uri: uri.href,
text: JSON.stringify({
status: "healthy",
uptime: process.uptime(),
timestamp: new Date().toISOString()
}, null, 2)
}]
})
);
// Resource from external source
server.resource(
"mcp://server/config",
"Application configuration",
{ mimeType: "application/json" },
async (uri) => {
const config = await loadConfigFromDatabase();
return {
contents: [{
uri: uri.href,
text: JSON.stringify(config, null, 2)
}]
};
}
);π¬ Adding Prompts
Prompts are templates that help structure AI interactions. Add them in src/prompts/index.ts:
// Simple prompt
server.prompt(
"explain-code",
"Explain what code does in simple terms",
{ code: z.string().describe("Code to explain") },
async ({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please explain what this code does in simple terms:\n\n\`\`\`\n${code}\n\`\`\``
}
}]
})
);
// Prompt with multiple parameters
server.prompt(
"code-review",
"Review code for potential issues",
{
code: z.string().describe("Code to review"),
language: z.string().optional().describe("Programming language"),
focus: z.enum(["security", "performance", "style", "all"]).optional().default("all")
.describe("What to focus on"),
},
async ({ code, language, focus }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Review this ${language || ''} code for ${focus} issues:\n\n\`\`\`${language || ''}\n${code}\n\`\`\`\n\nProvide specific suggestions for improvement.`
}
}]
})
);
// Multi-message prompt
server.prompt(
"debug-assistant",
"Help debug an issue",
{
error: z.string().describe("Error message or description"),
code: z.string().optional().describe("Relevant code"),
context: z.string().optional().describe("Additional context"),
},
async ({ error, code, context }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `I'm encountering this error: ${error}`
}
},
...(code ? [{
role: "user" as const,
content: {
type: "text" as const,
text: `Here's the relevant code:\n\`\`\`\n${code}\n\`\`\``
}
}] : []),
...(context ? [{
role: "user" as const,
content: {
type: "text" as const,
text: `Additional context: ${context}`
}
}] : []),
{
role: "user",
content: {
type: "text",
text: "Please help me understand and fix this issue."
}
}
]
})
);π Zod Schema Reference
Zod is used to validate tool inputs. Here's a quick reference:
Basic Types
z.string() // Any string
z.number() // Any number
z.boolean() // true or false
z.null() // null
z.undefined() // undefined
z.any() // Any type (avoid if possible)String Validations
z.string().min(1) // At least 1 character
z.string().max(100) // At most 100 characters
z.string().length(5) // Exactly 5 characters
z.string().email() // Valid email format
z.string().url() // Valid URL format
z.string().uuid() // Valid UUID
z.string().regex(/pattern/) // Matches regex
z.string().startsWith("Hi") // Starts with "Hi"
z.string().endsWith("!") // Ends with "!"Number Validations
z.number().min(0) // >= 0
z.number().max(100) // <= 100
z.number().positive() // > 0
z.number().negative() // < 0
z.number().int() // Integer only
z.number().multipleOf(5) // Divisible by 5Enums & Literals
z.enum(["a", "b", "c"]) // One of these values
z.literal("exact") // Exactly this value
z.union([z.string(), z.number()]) // String OR numberOptional & Defaults
z.string().optional() // String or undefined
z.string().nullable() // String or null
z.string().default("hi") // Default if not provided
z.string().optional().default("hi") // Optional with defaultArrays & Objects
z.array(z.string()) // Array of strings
z.array(z.number()).min(1) // Non-empty number array
z.array(z.string()).max(10) // At most 10 strings
z.object({ // Object shape
name: z.string(),
age: z.number().optional(),
})Descriptions (Important!)
// Always add descriptions - AI uses these!
z.string().describe("The user's email address")
z.number().min(1).max(5).describe("Rating from 1 to 5 stars")
z.enum(["asc", "desc"]).describe("Sort order")π Connect to AI Apps
VS Code (Copilot)
The .vscode/mcp.json file is already included. Update the path:
{
"servers": {
"mcp-server-starter": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/mcp-server-starter/build/index.js"]
}
}
}Then reload VS Code and your tools will be available to Copilot!
Claude Desktop (macOS)
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/absolute/path/to/mcp-server-starter/build/index.js"]
}
}
}Claude Desktop (Windows)
Edit %APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["C:\\path\\to\\mcp-server-starter\\build\\index.js"]
}
}
}With Environment Variables
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/build/index.js"],
"env": {
"API_KEY": "your-api-key",
"DATABASE_URL": "postgres://..."
}
}
}
}π Development Workflow
Daily Development
# Start watch mode (auto-rebuilds on changes)
npm run dev
# In another terminal, run the inspector
npm run inspectorTesting Changes
Make changes to your TypeScript files
Watch mode automatically rebuilds
In inspector, click "Reconnect" or refresh
Test your changes
Before Committing
# Full build to catch any issues
npm run build
# Test in inspector
npm run inspectorπ Debugging
Enable Debug Logging
Add to your code:
// Use console.error for logging (stdout is for MCP protocol)
console.error("[DEBUG] Tool called with:", params);Common Issues
Problem | Solution |
"Tool not found" | Did you register it in |
"Cannot find module" | Use |
Server won't start | Check for syntax errors with |
Inspector won't connect | Make sure no other server is running on the port |
Changes not showing | Rebuild with |
Viewing Protocol Messages
Run with debug output:
DEBUG=mcp* node build/index.jsβ Best Practices
Tool Design
Naming: Use
lowercase_with_underscores(e.g.,search_files,send_email)Single Purpose: Each tool should do one thing well
Clear Descriptions: AI reads these to decide when to use your tool
Parameter Descriptions: Always use
.describe()on every parameter
Error Handling
Always validate inputs even though Zod helps
Return
isError: truefor failures so AI knows something went wrongProvide helpful error messages that explain what went wrong
Handle edge cases (empty strings, zero values, null, etc.)
Performance
Keep tools fast - AI waits for responses
Use timeouts for network requests
Cache when appropriate for repeated operations
Stream large responses if supported
Security
Never expose secrets through tool responses
Validate file paths to prevent directory traversal
Sanitize user input before using in commands/queries
Use environment variables for sensitive configuration
β FAQ
Q: Can I use JavaScript instead of TypeScript?
Yes! Remove tsconfig.json, rename files to .js, and update package.json:
"scripts": {
"start": "node src/index.js"
}Q: How do I add npm packages?
npm install package-name
npm install -D @types/package-name # For TypeScript typesThen import and use in your tools.
Q: Can one tool call another tool?
Yes, but it's better to extract shared logic into a regular function:
// Shared logic
function calculateTax(amount: number, rate: number): number {
return amount * rate;
}
// Tool 1 uses it
server.tool("calc_sales_tax", ..., async ({ amount }) => {
const tax = calculateTax(amount, 0.08);
return { content: [{ type: "text", text: `Tax: $${tax}` }] };
});
// Tool 2 also uses it
server.tool("calc_income_tax", ..., async ({ income }) => {
const tax = calculateTax(income, 0.25);
return { content: [{ type: "text", text: `Tax: $${tax}` }] };
});Q: How do I return structured data?
Return JSON as a string:
server.tool("get_user", ..., async ({ id }) => {
const user = await fetchUser(id);
return {
content: [{
type: "text",
text: JSON.stringify(user, null, 2) // Pretty-printed JSON
}]
};
});Q: Can I return images or files?
Yes, using base64 encoding:
server.tool("get_chart", ..., async (params) => {
const imageBuffer = await generateChart(params);
return {
content: [{
type: "image",
data: imageBuffer.toString("base64"),
mimeType: "image/png"
}]
};
});Q: How do I handle long-running operations?
MCP supports progress notifications for long operations:
server.tool("long_task", ..., async (params, { sendProgress }) => {
for (let i = 0; i <= 100; i += 10) {
await doPartialWork();
await sendProgress({ progress: i, total: 100 });
}
return { content: [{ type: "text", text: "Done!" }] };
});π Commands Reference
Command | Description |
| Install dependencies |
| Compile TypeScript to JavaScript |
| Watch mode - auto-rebuild on changes |
| Run the compiled server directly |
| Open interactive testing UI |
π Resources
Official Documentation
MCP Documentation - Full protocol docs
MCP Specification - Technical spec
TypeScript SDK - SDK source
Examples & Inspiration
Official MCP Servers - Reference implementations
Awesome MCP - Community servers
Related Tools
Zod Documentation - Schema validation
MCP Inspector - Testing tool
π€ Contributing
Contributions welcome! Please:
Fork the repository
Create a feature branch
Make your changes
Submit a pull request
π License
MIT - Use this however you want!
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/aniketbiswas/mcp-server-starter'
If you have feedback or need assistance with the MCP directory API, please join our Discord server