Skip to main content

What You’ll Build

A simple MCP server with one tool that you can deploy and register on the Context Marketplace. No external API keys required.
Already have an API you want to unbundle? Skip to the AI-Assisted Builder section in the Build & List guide for a faster workflow using Cursor or Claude.

Prerequisites

Before starting, ensure you have:
  • Node.js 18+ — Check with node --version
  • pnpm (recommended) — Install with npm install -g pnpm
  • A code editor — VS Code, Cursor, etc.

Step 1: Create Your Project

Open your terminal and run these commands:
# Create project directory
mkdir my-first-mcp
cd my-first-mcp

# Initialize the project
pnpm init

# Install dependencies
pnpm add @modelcontextprotocol/sdk @ctxprotocol/sdk express dotenv
pnpm add -D typescript @types/node @types/express tsx
Create a tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["*.ts"]
}
Update your package.json to add the start script:
{
  "name": "my-first-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch server.ts",
    "start": "tsx server.ts"
  }
}

Step 2: Write Your MCP Server (Without Security)

We’ll build the server without the security middleware first so you can test everything locally. You’ll add security before deploying. Create server.ts:
import "dotenv/config";
import { randomUUID } from "node:crypto";
import express, { type Request, type Response } from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  isInitializeRequest,
} from "@modelcontextprotocol/sdk/types.js";

// ============================================================================
// TOOL DEFINITIONS
// ============================================================================

const QUOTES = [
  { quote: "The only way to do great work is to love what you do.", author: "Steve Jobs" },
  { quote: "Innovation distinguishes between a leader and a follower.", author: "Steve Jobs" },
  { quote: "Stay hungry, stay foolish.", author: "Steve Jobs" },
  { quote: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" },
  { quote: "It is during our darkest moments that we must focus to see the light.", author: "Aristotle" },
  { quote: "The best time to plant a tree was 20 years ago. The second best time is now.", author: "Chinese Proverb" },
  { quote: "Your time is limited, don't waste it living someone else's life.", author: "Steve Jobs" },
  { quote: "The only impossible journey is the one you never begin.", author: "Tony Robbins" },
];

const TOOLS = [
  {
    name: "get_random_quote",
    description: "Get a random inspirational quote. Perfect for motivation and daily inspiration.",
    inputSchema: {
      type: "object" as const,
      properties: {},
      required: [],
    },
    // outputSchema is REQUIRED by Context for dispute resolution
    outputSchema: {
      type: "object" as const,
      properties: {
        quote: { type: "string", description: "The inspirational quote" },
        author: { type: "string", description: "Who said it" },
        fetchedAt: { type: "string", description: "ISO timestamp" },
      },
      required: ["quote", "author", "fetchedAt"],
    },
  },
];

// ============================================================================
// MCP SERVER SETUP
// ============================================================================

const server = new Server(
  { name: "my-first-mcp", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// Handle tools/list - returns tool definitions
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: TOOLS,
}));

// Handle tools/call - executes tool and returns result
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name } = request.params;

  if (name === "get_random_quote") {
    const randomIndex = Math.floor(Math.random() * QUOTES.length);
    const selected = QUOTES[randomIndex];
    
    const result = {
      quote: selected.quote,
      author: selected.author,
      fetchedAt: new Date().toISOString(),
    };

    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
      structuredContent: result, // REQUIRED by Context
    };
  }

  return {
    content: [{ type: "text", text: `Unknown tool: ${name}` }],
    isError: true,
  };
});

// ============================================================================
// EXPRESS SERVER (no security middleware yet - for local testing)
// ============================================================================

const app = express();
app.use(express.json());

// Session management
const transports: Record<string, StreamableHTTPServerTransport> = {};

// Health check endpoint
app.get("/health", (_req: Request, res: Response) => {
  res.json({ status: "ok", server: "my-first-mcp", version: "1.0.0" });
});

// MCP endpoint (no auth - we'll add it before deploying)
app.post("/mcp", async (req: Request, res: Response) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  let transport: StreamableHTTPServerTransport;

  if (sessionId && transports[sessionId]) {
    transport = transports[sessionId];
  } else if (!sessionId && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (id) => {
        transports[id] = transport;
      },
    });
    await server.connect(transport);
  } else {
    res.status(400).json({ error: "Invalid session" });
    return;
  }

  await transport.handleRequest(req, res, req.body);
});

app.get("/mcp", async (req: Request, res: Response) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = transports[sessionId];
  if (transport) {
    await transport.handleRequest(req, res);
  } else {
    res.status(400).json({ error: "Invalid session" });
  }
});

// Start server
const PORT = Number(process.env.PORT || 3000);
app.listen(PORT, () => {
  console.log(`🚀 My First MCP Server running on http://localhost:${PORT}`);
  console.log(`📡 MCP endpoint: http://localhost:${PORT}/mcp`);
  console.log(`💚 Health check: http://localhost:${PORT}/health`);
});

Step 3: Test Locally

Start your server:
pnpm dev
You should see:
🚀 My First MCP Server running on http://localhost:3000
📡 MCP endpoint: http://localhost:3000/mcp
💚 Health check: http://localhost:3000/health
Test the health endpoint:
curl http://localhost:3000/health
Test initialize (start a session):
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": { "name": "test", "version": "1.0.0" }
    },
    "id": 1
  }'
Copy the sessionId from the response. Test tools/call (execute your tool):
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "mcp-session-id: YOUR-SESSION-ID" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "get_random_quote",
      "arguments": {}
    },
    "id": 2
  }'
You should get a random quote back. Your tool works locally.

Step 4: Add Security Middleware

Before deploying, you must add the Context security middleware. This ensures only paid requests from the Context Platform can execute your tools. Update your server.ts:
// Add this import at the top
import { createContextMiddleware } from "@ctxprotocol/sdk";

// Add this after the transports declaration
const verifyContextAuth = createContextMiddleware();

// Update your MCP endpoints to use the middleware:
app.post("/mcp", verifyContextAuth, async (req: Request, res: Response) => {
  // ... rest of handler stays the same
});

app.get("/mcp", verifyContextAuth, async (req: Request, res: Response) => {
  // ... rest of handler stays the same
});
Don’t skip this step! Without the middleware, anyone can call your tools for free. The middleware verifies that requests come from the Context Platform with valid payment.
After adding the middleware, tools/call will return {"error":"Unauthorized"} when tested locally. This is expected — it means security is working. You can always comment out verifyContextAuth temporarily for local testing.

Step 5: Deploy to Railway

HTTPS is required. The Context Platform will only connect to HTTPS endpoints. Railway, Vercel, and similar platforms provide HTTPS automatically. If you’re self-hosting, you’ll need to set up HTTPS (e.g., with Caddy or nginx).
1

Push to GitHub

Create a new repository and push your code:
git init
git add .
git commit -m "Initial commit"
gh repo create my-first-mcp --public --push
2

Deploy on Railway

  1. Go to railway.app and sign in with GitHub
  2. Click “New Project”“Deploy from GitHub repo”
  3. Select your my-first-mcp repository
  4. Railway auto-detects Node.js and deploys
3

Configure Start Command

In Railway dashboard:
  1. Go to your service Settings
  2. Set Start Command to: pnpm start
  3. Railway will redeploy automatically
4

Get Your Public URL

  1. Go to SettingsNetworking
  2. Click “Generate Domain”
  3. Copy your URL (e.g., https://my-first-mcp-production.up.railway.app)
Test your deployed server:
curl https://YOUR-RAILWAY-URL.up.railway.app/health

Step 6: Register on Context Marketplace

1

Go to Contribute Page

2

Fill in the Form

FieldValue
NameMy First MCP (or your preferred name)
DescriptionRandom inspirational quotes for motivation and daily inspiration.
CategoryUtility
Price$0.00 (start free to test)
Endpointhttps://YOUR-RAILWAY-URL.up.railway.app/mcp
3

Auto-Discovery

Click submit. Context will call your /mcp endpoint to discover your tools via listTools().
4

Stake USDC

All tools require a minimum $10 USDC stake. This is fully refundable with a 7-day withdrawal delay.

Step 7: Verify It Works

  1. Go to ctxprotocol.com
  2. In the chat, ask: “Get me an inspirational quote”
  3. The agent should discover and use your tool!

Updating Your Tool

When you add new endpoints or modify your MCP server, you need to refresh the tool listing on Context.
1

Deploy Your Changes

Push your updated code to your deployment (Railway, VPS, etc.)
2

Refresh Skills

  1. Go to ctxprotocol.com/developer/tools
  2. Find your tool and click “Refresh Skills”
  3. Context will re-call listTools() to discover your new/updated tools
3

Update Description (Optional)

If you’ve added significant new functionality:
  1. Use the MCP Server Analysis Prompt to generate an updated description
  2. Edit your tool’s description in the Developer Tools page
Common mistake: Deploying new endpoints but forgetting to click “Refresh Skills”. Your new tools won’t appear in the marketplace until you refresh.

What’s Next?


Troubleshooting

For a complete list of common errors and solutions, see the Troubleshooting Guide.
This means your security middleware is working correctly.After adding createContextMiddleware() (Step 4), local tools/call requests will return Unauthorized because they don’t have a valid JWT from the Context Platform.If testing locally:
  • Temporarily comment out verifyContextAuth in your endpoint handlers
  • Or test before adding the middleware (Step 3)
If deployed and registered but still getting Unauthorized:
  • Ensure your endpoint is HTTPS (not HTTP)
  • Verify the URL you registered matches your deployed server exactly
  • Check that your tool is actually registered at ctxprotocol.com/contribute
  • Check Node.js version: node --version (must be 18+)
  • Ensure all dependencies installed: pnpm install
  • Check for TypeScript errors: pnpm exec tsc --noEmit
  • Ensure package.json has "type": "module"
  • Check Railway logs for specific errors
  • Verify start command is pnpm start
  • Verify your /health endpoint returns 200
  • Test initialize and tools/list with curl (these don’t require auth)
  • Ensure HTTPS URL — HTTP endpoints will not work

Complete Project Structure

Your project should look like this:
my-first-mcp/
├── package.json
├── tsconfig.json
├── server.ts
└── node_modules/
Ready for more? Check out the complete server examples for production patterns including error handling, caching, and multiple tools.