Building an MCP Tool Server
This tutorial walks through building an MCP (Model Context Protocol) server with custom tools that AI assistants like Claude can discover and call.
Prerequisites
- Scriptling CLI installed (Installation)
- An MCP-compatible AI client (e.g., Claude Desktop)
What You’ll Build
A tool server that exposes three tools:
- time_now: returns the current time in a given timezone format
- data_summary: generates a summary of a list of numbers
- format_report: formats key-value data as a structured report
Step 1: Create the Tool Directory
mkdir -p my-tools
cd my-toolsStep 2: Define the time_now Tool
Create my-tools/time_now.toml:
description = "Get the current date and time"
keywords = ["time", "date", "now"]
[[parameters]]
name = "format"
type = "string"
description = "Time format string (e.g. '%Y-%m-%d' for date, '%H:%M:%S' for time)"
required = falseCreate my-tools/time_now.py:
import time
def call(args, kwargs):
fmt = kwargs.get("format", "%Y-%m-%d %H:%M:%S")
return time.strftime(fmt, time.localtime())Step 3: Define the data_summary Tool
Create my-tools/data_summary.toml:
description = "Calculate summary statistics for a list of numbers"
keywords = ["statistics", "stats", "average", "mean", "data"]
[[parameters]]
name = "numbers"
type = "array:number"
description = "List of numbers to summarize"
required = trueCreate my-tools/data_summary.py:
import statistics
def call(args, kwargs):
numbers = kwargs["numbers"]
if not numbers:
return {"error": "Empty list provided"}
result = {
"count": len(numbers),
"sum": sum(numbers),
"mean": statistics.mean(numbers),
"min": min(numbers),
"max": max(numbers),
}
if len(numbers) > 1:
result["stdev"] = statistics.stdev(numbers)
result["median"] = statistics.median(numbers)
return resultStep 4: Define the format_report Tool
Create my-tools/format_report.toml:
description = "Format a dictionary of data as a structured text report"
keywords = ["format", "report", "text", "display"]
[[parameters]]
name = "title"
type = "string"
description = "Report title"
required = true
[[parameters]]
name = "data"
type = "string"
description = "JSON string of key-value pairs to include in the report"
required = trueCreate my-tools/format_report.py:
import json
def call(args, kwargs):
title = kwargs["title"]
data = json.loads(kwargs["data"])
lines = []
lines.append("=" * 50)
lines.append(f" {title}")
lines.append("=" * 50)
lines.append("")
for key, value in data.items():
lines.append(f" {key}:")
lines.append(f" {value}")
lines.append("")
lines.append("-" * 50)
return "\n".join(lines)Step 5: Start the MCP Server
Scriptling serves MCP over HTTP. Start the server with the tools directory:
scriptling --server :8000 --mcp-tools ./my-tools setup.pyThe MCP endpoint is mounted at http://127.0.0.1:8000/mcp. See MCP Server Mode for the full set of options.
Step 6: Configure Claude Desktop
Add the tool server to your Claude Desktop configuration (claude_desktop_config.json) using the HTTP endpoint:
{
"mcpServers": {
"scriptling-tools": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}Restart Claude Desktop. The AI will discover the tools automatically and can call them when needed.
Step 7: Test Tools from the CLI
Call individual tools directly against the /mcp endpoint with a tools/call request:
# Test time_now
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"my-tools/time_now","arguments":{"format":"%H:%M"}}}'
# Test data_summary
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"my-tools/data_summary","arguments":{"numbers":[10,20,30,40,50]}}}'To list all available tools, send a tools/list request:
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/list"}'Step 8: Add Discoverable Tools
By default (discoverable = false) a tool appears in tools/list and the client can call it directly. With a large tool set, set discoverable = true to keep a tool out of tools/list: the client finds it through the built-in tool_search tool (matching its description and keywords) and runs it through execute_tool:
description = "Query the internal database"
keywords = ["database", "sql", "query"]
discoverable = true
[[parameters]]
name = "query"
type = "string"
description = "SQL-like query string"
required = trueNo registration code is needed: discoverable tools live in the tools directory like any other. For tools that should only exist for some callers or depend on runtime state, register them per request from middleware with mcp.register_request_tool().
Tool Directory Structure
The final directory structure:
my-tools/
time_now.toml
time_now.py
data_summary.toml
data_summary.py
format_report.toml
format_report.pyWhat You Learned
- Defining MCP tools with TOML metadata files
- Implementing tool logic in Scriptling scripts
- Starting Scriptling as an MCP server
- Configuring Claude Desktop to use custom tools
- Testing tools from the command line
- Native vs discoverable tools
See Also
- Writing MCP Tools - Complete MCP tool reference
- MCP Client Library - Connecting to MCP servers from scripts
- CLI MCP Server - CLI server mode documentation
- Statistics Library - Stats functions used in this tutorial