The Challenge: Insecure Tool Schemas
The Model Context Protocol (MCP) relies on JSON schemas to define tool capabilities. If a schema is tampered with, an attacker can hijack your AI agent's tools. This is a critical supply chain vulnerability.
SchemaPin solves this by using cryptographic signatures to guarantee that tool schemas are authentic and unmodified.
Step 1: Install CLI Tools
The command-line tools for signing schemas are distributed via Pip. They are a required first step, even for a JavaScript project.
pip install schemapin
Step 2: Create & Sign Your Schema
Create a file named get_issue_schema.json
with the tool's definition.
{
"name": "get_issue",
"description": "Gets the contents of an issue within a repository",
"input_schema": {
"type": "object",
"properties": {
"owner": { "type": "string", "description": "Repository owner" },
"repo": { "type": "string", "description": "Repository name" },
"issue_number": { "type": "number", "description": "Issue number" }
},
"required": ["owner", "repo", "issue_number"]
}
}
Generate your cryptographic key pair.
schemapin-keygen
Now, use your private key to sign the schema, producing a secure, signed version.
schemapin-sign --schema get_issue_schema.json \
--key private.key \
--output get_issue_signed.json
Step 3: Host Public Key for Discovery
For services to verify your signatures, they must be able to find your public key. The standard approach is to host it at a .well-known/schemapin.json
URL on your domain. Use the automated well-known server to host your public key.
Run the well-known server, which automatically creates the proper directory structure and serves your public key. Keep this server running in a separate terminal.
python -m schemapin.well_known_server --public-key-file public.key --port 8000
Step 4: Build the Secure JavaScript Server
Setup
npm init -y
npm install express schemapin
touch server.js
Add to server.js
const express = require('express');
const fs = require('fs').promises;
const { verify, discoverPublicKey } = require('schemapin');
const TOOL_SCHEMAS = {};
async function loadAndVerifySchema(path, domain, keyId) {
console.log(`Verifying ${path}...`);
try {
const signedSchema = JSON.parse(await fs.readFile(path, 'utf-8'));
console.log(`Discovering key from ${domain}...`);
const publicKey = await discoverPublicKey(domain, keyId);
const isVerified = await verify(signedSchema, publicKey);
if (!isVerified) throw new Error("Signature invalid.");
console.log(`SUCCESS: Verified '${signedSchema.payload.name}'.`);
return signedSchema.payload;
} catch (error) {
console.error(`ERROR: Verification failed!`, error.message);
return null;
}
}
async function main() {
const app = express();
const verifiedTool = await loadAndVerifySchema(
'get_issue_signed.json',
'http://localhost:8000',
'default'
);
if (verifiedTool) {
TOOL_SCHEMAS[verifiedTool.name] = verifiedTool;
} else {
console.log("\nShutting down due to verification failure.");
process.exit(1);
}
app.get('/tools', (req, res) => res.json(Object.keys(TOOL_SCHEMAS)));
app.listen(5002, () => console.log('JS MCP server on port 5002'));
}
main();
Run and Test Security
Run node server.js
. Then, stop the server, tamper with the signature in get_issue_signed.json
, and run it again to see the failure.
Verifying get_issue_signed.json...
Discovering key from http://localhost:8000...
SUCCESS: Verified 'get_issue'.
JS MCP server on port 5002