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:

Project structure
my-strumento/
├── Dockerfile           # Container definition
├── manifest.json        # Tool definitions and metadata
├── package.json         # Dependencies (Node.js)
├── src/
│   └── index.ts        # Main entry point
└── README.md

Step 1: Create the Manifest

The manifest.json defines your plugin metadata and available tools:

manifest.json
{
  "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:

src/index.ts
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

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

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 and run locally
# 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:latest

Step 6: Register the Plugin

Add your plugin definition to the registry:

Plugin definition
{
  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:

Calling GPU services
// 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

PluginDescriptionComplexity
filesystemRead/write files with path restrictionsSimple
postgresExecute SQL queries safelyMedium
githubFull GitHub API integrationComplex