Building Strumenti
Create custom MCP tools (strumenti) to extend IntelligenceBox with new capabilities. This guide walks you through building, packaging, and deploying a custom plugin.
Project Structure
A typical MCP strumento has the following structure:
my-strumento/
├── Dockerfile # Container definition
├── manifest.json # Tool definitions and metadata
├── package.json # Dependencies (Node.js)
├── src/
│ └── index.ts # Main entry point
└── README.mdStep 1: Create the Manifest
The manifest.json defines your plugin metadata and available tools:
{
"name": "my-weather-tool",
"version": "1.0.0",
"description": "Get current weather for any city",
"author": "Your Name",
"tools": [
{
"name": "get_weather",
"description": "Get current weather conditions for a city",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name (e.g., 'Rome', 'New York')"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius",
"description": "Temperature units"
}
},
"required": ["city"]
}
}
]
}Step 2: Implement the Tool
Create the main entry point that handles MCP protocol messages:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Create MCP server
const server = new Server(
{ name: "my-weather-tool", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description: "Get current weather conditions for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
units: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
const { city, units = "celsius" } = args as { city: string; units?: string };
// Call your weather API here
const weather = await fetchWeather(city, units);
return {
content: [
{
type: "text",
text: JSON.stringify(weather, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Helper function (implement your actual API call)
async function fetchWeather(city: string, units: string) {
// Example: Call OpenWeatherMap API
const apiKey = process.env.WEATHER_API_KEY;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units === 'celsius' ? 'metric' : 'imperial'}&appid=${apiKey}`
);
return response.json();
}
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running");
}
main().catch(console.error);Step 3: Package.json
{
"name": "my-weather-tool",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}Step 4: Create Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source and build
COPY . .
RUN npm run build
# MCP servers communicate via stdio
CMD ["node", "dist/index.js"]Step 5: Build and Test
# Build the Docker image
docker build -t my-weather-tool:latest .
# Test locally (interactive mode)
echo '{"method":"tools/list","params":{},"id":1}' | \
docker run -i --rm -e WEATHER_API_KEY=xxx my-weather-tool:latest
# Test a tool call
echo '{"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"Rome"}},"id":2}' | \
docker run -i --rm -e WEATHER_API_KEY=xxx my-weather-tool:latestStep 6: Register the Plugin
Add your plugin definition to the registry:
{
id: 'my-weather-tool',
name: 'Weather Tool',
description: 'Get current weather for any city',
author: 'Your Name',
icon: '🌤️',
category: 'utilities',
tags: ['weather', 'api', 'data'],
dockerImage: 'my-weather-tool:latest',
configSchema: {
apiKey: {
type: 'string',
description: 'OpenWeatherMap API key',
required: true
}
},
requirements: {
minMemory: '128Mi',
minCpu: 0.1,
capabilities: ['network']
}
}Using GPU Services
Your MCP tool can call IntelligenceBox GPU services for AI operations:
// Inside your MCP tool, call GPU services via HTTP
async function extractEntities(text: string) {
const response = await fetch('http://gliner:8093/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
labels: ['person', 'company', 'location'],
threshold: 0.5
})
});
return response.json();
}
// Use in your tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "analyze_document") {
const { text } = request.params.arguments as { text: string };
// Use GLiNER for entity extraction
const entities = await extractEntities(text);
return {
content: [{
type: "text",
text: JSON.stringify(entities, null, 2)
}]
};
}
});Best Practices
Error Handling
Always return meaningful error messages. The LLM uses these to understand what went wrong.
try {
const result = await callExternalApi();
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error.message}. Please check the city name and try again.`
}],
isError: true
};
}Structured Output
Return structured JSON for complex data. The LLM can parse and reason about it better.
Security
Never log or expose API keys. Use environment variables for all secrets. Validate and sanitize all inputs before using them.
Example Projects
| Plugin | Description | Complexity |
|---|---|---|
filesystem | Read/write files with path restrictions | Simple |
postgres | Execute SQL queries safely | Medium |
github | Full GitHub API integration | Complex |