In this guide, we'll walk through creating a Model Context Protocol (MCP) server from scratch. We'll use the Plausible Analytics API as our example, but the principles can be applied to any API you want to make accessible to AI models. Additionally, we will explain the process of deploying the MCP Server. For this, we use
smithery.ai.
What is a Model Context Protocol Server?#
A Model Context Protocol server acts as a bridge between AI models (like Claude) and external APIs or services. It provides a standardized way for AI models to interact with these services through well-defined tools and interfaces. If you want to learn more about it, head over to the
Official Reference Documentation:.
Prerequisites#
To follow this guide, you'll need:
- Node.js installed on your system
- Basic understanding of TypeScript
- Basic understanding of APIs and HTTP requests
- A Plausible Analytics API key (for this specific example)
Project Setup#
First, let's create a new project and install the necessary dependencies:
bash
1mkdir mcp-server
2cd mcp-server
3npm init -y
Update your package.json
with the following configuration:
json
1{
2 "name": "plausible-model-context-protocol-server",
3 "version": "0.0.1",
4 "description": "MCP Server for Plausible Analytics",
5 "license": "MIT",
6 "type": "module",
7 "bin": {
8 "mcp-plausible-server": "dist/index.js"
9 },
10 "files": [
11 "dist"
12 ],
13 "scripts": {
14 "build": "tsc && shx chmod +x dist/*.js",
15 "prepare": "npm run build",
16 "watch": "tsc --watch"
17 },
18 "dependencies": {
19 "@modelcontextprotocol/sdk": "1.7.0",
20 "@types/node": "^22"
21 },
22 "devDependencies": {
23 "@types/node": "^22",
24 "shx": "^0.3.4",
25 "typescript": "^5.8.2"
26 }
27}
The key dependency here is
@modelcontextprotocol/sdk
, which provides the core functionality for creating an MCP server. You can read more about it
here.
TypeScript Configuration#
Create a tsconfig.json
file. This is necessary to tell the tsc
(TypeScript Transpiler) how to handle the TypeScript code and transpile it into JavaScript code.
json
1{
2 "compilerOptions": {
3 "target": "es2020",
4 "module": "es2020",
5 "moduleResolution": "node",
6 "outDir": "./dist",
7 "rootDir": "./src",
8 "strict": true,
9 "esModuleInterop": true,
10 "skipLibCheck": true,
11 "forceConsistentCasingInFileNames": true
12 },
13 "include": ["src/**/*"],
14 "exclude": ["node_modules", "dist"]
15}
Component-by-Component Implementation#
In order to create a MCP server, a few components are necessary. We've split the logic into three main topics: API Client, Tool Definition and the MCP Server.
1. API Client (src/plausible/client.ts
)#
The API client handles the actual communication with the external service. It's responsible for making HTTP requests and handling responses. Here, you can add the actual interaction logic and handle requests and responses.
typescript
1const PLAUSIBLE_API_URL = process.env.PLAUSIBLE_API_URL || "https://plausible.io/api/v2";
2const PLAUSIBLE_API_KEY = process.env.PLAUSIBLE_API_KEY;
3
4if (!PLAUSIBLE_API_KEY) {
5 throw new Error("PLAUSIBLE_API_KEY environment variable is required");
6}
7
8class PlausibleClient {
9 async query(siteId: string, metrics: string[], dateRange: string) {
10 const response = await fetch(`${PLAUSIBLE_API_URL}/query`, {
11 method: "POST",
12 headers: {
13 Authorization: `Bearer ${PLAUSIBLE_API_KEY}`,
14 "Content-Type": "application/json",
15 },
16 body: JSON.stringify({
17 site_id: siteId,
18 metrics: metrics,
19 date_range: dateRange,
20 }),
21 });
22
23 if (!response.ok) {
24 throw new Error(`Plausible API error: ${response.statusText}`);
25 }
26
27 return response.json();
28 }
29}
30
31export const plausibleClient = new PlausibleClient();
Key points:
- Uses environment variables for configuration
- Implements API-specific methods (in this case,
query
)
- Handles errors appropriately
- Exports a singleton instance
2. Tool Definition (src/plausible/query.ts
)#
The tool definition describes how AI models can interact with your service. It follows the standards set by the Model Context Protocol
documentation.
typescript
1import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
3export interface QueryArgs {
4 site_id: string;
5 metrics: string[];
6 date_range: string;
7}
8
9export const queryTool: Tool = {
10 name: "plausible_query",
11 description: "Query analytics data from Plausible",
12 inputSchema: {
13 type: "object",
14 required: ["site_id", "metrics", "date_range"],
15 properties: {
16 site_id: {
17 type: "string",
18 description: "The domain of the site to query data for",
19 },
20 metrics: {
21 type: "array",
22 items: {
23 type: "string",
24 },
25 description: "List of metrics to query (e.g., visitors, pageviews)",
26 },
27 date_range: {
28 type: "string",
29 description: "Date range for the query (e.g., '7d', '30d')",
30 },
31 },
32 },
33};
Key points:
- Defines the tool's interface using TypeScript
- Specifies required and optional parameters
- Provides clear descriptions for each parameter
- Uses JSON Schema for parameter validation
3. MCP Server (src/index.ts
)#
The main server file ties everything together. It's responsible for exposing the MCP server, handling incoming requests and responding accordingly.
typescript
1#!/usr/bin/env node
2import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4import {
5 CallToolRequest,
6 CallToolRequestSchema,
7 ListToolsRequestSchema,
8} from "@modelcontextprotocol/sdk/types.js";
9import { QueryArgs, queryTool } from "./plausible/query.js";
10import { plausibleClient } from "./plausible/client.js";
11
12const server = new Server(
13 {
14 name: "plausible-model-context-protocol-server",
15 version: "0.0.1",
16 },
17 {
18 capabilities: {
19 tools: {},
20 },
21 }
22);
23
24server.setRequestHandler(ListToolsRequestSchema, async () => {
25 return {
26 tools: [queryTool],
27 };
28});
29
30server.setRequestHandler(
31 CallToolRequestSchema,
32 async (request: CallToolRequest) => {
33 try {
34 if (!request.params.arguments) {
35 throw new Error("Arguments are required");
36 }
37
38 switch (request.params.name) {
39 case "plausible_query": {
40 const args = request.params.arguments as unknown as QueryArgs;
41 if (!args.site_id || !args.metrics || !args.date_range) {
42 throw new Error("Missing required arguments");
43 }
44 const response = await plausibleClient.query(
45 args.site_id,
46 args.metrics,
47 args.date_range
48 );
49 return {
50 content: [{ type: "text", text: JSON.stringify(response) }],
51 };
52 }
53
54 default:
55 throw new Error(`Unknown tool: ${request.params.name}`);
56 }
57 } catch (error) {
58 console.error("Error executing tool:", error);
59 return {
60 content: [
61 {
62 type: "text",
63 text: JSON.stringify({
64 error: error instanceof Error ? error.message : String(error),
65 }),
66 },
67 ],
68 };
69 }
70 }
71);
72
73async function runServer() {
74 const transport = new StdioServerTransport();
75 await server.connect(transport);
76 console.error("Plausible MCP Server running on stdio");
77}
78
79runServer().catch((error) => {
80 console.error("Fatal error in main():", error);
81 process.exit(1);
82});
Key points:
- Creates an MCP server instance
- Registers available tools
- Handles tool execution requests
- Uses stdio for communication
- Implements error handling
How It All Works Together#
In order to provide you with a complete picture, the following steps explain how everything works together:
- The AI model sends a request to execute a tool (e.g.,
plausible_query
)
- The request comes through
stdio
to your server
- The server validates the request and its arguments
- The appropriate tool handler is called
- The tool uses the API client to make the actual request
- The response is formatted and sent back to the AI model
Using the Server#
- Build the server:
bash
1npm install
2npm run build
- Configure the environment:
bash
1export PLAUSIBLE_API_KEY=your-api-key
2export PLAUSIBLE_API_URL=your-plausible-url
- Run the server:
Integration with Claude Desktop#
In order to test the MCP server with an actual LLM AI model,
Claude Desktop offers an option to test and run local MCP servers. This can be achieved by adding the following JSON block to the configuration of your Claude Desktop MCP Server config
file:
json
1{
2 "mcpServers": {
3 "mcp-plausible-local": {
4 "command": "node",
5 "args": ["/path/to/project/dist/index.js"],
6 "env": {
7 "PLAUSIBLE_API_URL": "https://plausible.io/api/v2",
8 "PLAUSIBLE_API_KEY": "your_api_key"
9 }
10 }
11 }
12}
Best Practices#
- Error Handling: Always implement proper error handling and provide meaningful error messages.
- Type Safety: Use TypeScript interfaces and type checking to catch errors early.
- Documentation: Provide clear descriptions for your tools and their parameters.
- Environment Variables: Use environment variables for configuration and sensitive data.
- Validation: Validate all input parameters before making API calls.
Deployment#
Currently, the process of deploying a Model Context Protocol Server is quite new in terms of how and where to deploy it to. One of the main players is
smithery.ai, who provide a all in one solution for deploying and exploring MCP servers. In order to deploy the following implementation to
smithery.ai, you need to ensure the following two things:
- Your repository is publicly available.
- A smithery.yaml and a Dockerfile is available.
Therefore, let's create the two files:
yaml
1startCommand:
2 type: stdio
3 configSchema:
4 type: object
5 required:
6 - plausibleAPIKey
7 - plausibleAPIURL
8 properties:
9 plausibleAPIKey:
10 type: string
11 description: The API Key of your Plausible account.
12 plausibleAPIURL:
13 type: string
14 description: The URL of your Plausible server, default to https://plausible.io/api/v2
15 commandFunction:
16 |-
17 (config) => ({ command: 'node', args: ['dist/index.js'], env: { PLAUSIBLE_API_URL: config.plausibleAPIURL, PLAUSIBLE_API_KEY: config.plausibleAPIKey } })
Here, you can define the necessary properties and provide the startup command.
Additionally, the Dockerfile tells
smithery.ai how to build the server:
Dockerfile
1FROM node:23-alpine
2
3WORKDIR /app
4
5COPY . ./
6
7# Install dependencies
8RUN npm install
9
10# Build the application
11RUN npm run build
12
13# Command will be provided by smithery.yaml
14CMD ["node", "dist/index.js"]
15
Once these files are created, head over to
smithery.ai, connect your GitHub account and hit the "Add Server" button.

The Add Server section of smithery.ai
smithery.ai will ask you to select the repository and once all is connected, you need to start a
Deployment
. Once thats done, your MCP server is live and findable via
smithery.ai.

The deployment section of smithery.ai
Conclusion#
Creating an MCP server is a powerful way to extend AI models' capabilities by giving them access to external services. The key is to:
- Define clear interfaces for your tools
- Handle errors gracefully
- Provide good documentation
- Follow TypeScript best practices
With this guide, you should be able to create your own MCP server for any API or service you want to make accessible to AI models. You can find the repository of this guide
here and the deployed MCP server
here.
For organizations looking to implement MCP in their AI solutions, our
agency specializes in developing custom MCP servers and integrations tailored to specific business needs. We help bridge the gap between your organization's knowledge repositories and the AI systems your users interact with.
Note: The Model Context Protocol is a rapidly evolving technology. The information in this article is current as of March 2025. For the most up-to-date information, please refer to the official MCP specification and documentation.