ai-supply.store
PublishSign in
← Community
Tutorials

Writing a safe MCP server for the marketplace

@ai-supply · 41m ago

Writing a safe MCP server for the marketplace

MCP servers run inside agent runtimes with direct access to the host's tools, files, and network. That power comes with responsibility. This guide shows you how to build an MCP server that earns a grade A on the ai-supply.store security scanner.

Structure your project correctly

A minimal safe MCP server project:

my-mcp-server/
├── package.json        # pinned, audited dependencies
├── server.ts           # entry point
├── tools/              # one file per tool
│   └── search.ts
├── .gitignore          # must include .env
└── README.md

Keep each tool in its own file. This makes the scanner's analysis more accurate and the code easier to review.

Never hardcode secrets

The scanner checks for secrets patterns across all files. Even a commented-out API key will trigger a finding.

// BAD — will fail the secrets check
const apiKey = "sk-live-abc123";

// GOOD — read from environment
const apiKey = process.env.MY_SERVICE_API_KEY;
if (!apiKey) throw new Error("MY_SERVICE_API_KEY is required");

Document required environment variables in your README so buyers know what to set.

Validate every input

LLM01 (prompt injection) is the most common finding for MCP servers. The fix is strict input validation:

import { z } from "zod";

const SearchInput = z.object({
  query: z.string().min(1).max(500),
  limit: z.number().int().min(1).max(50).default(10),
});

server.tool("search", SearchInput, async ({ query, limit }) => {
  // query is now a validated, bounded string — not a raw prompt
  const results = await mySearchService(query, limit);
  return { results };
});

Never pass raw tool arguments directly to shell commands, SQL queries, or template strings.

Limit your permissions (LLM08)

LLM08 (excessive agency) penalises capabilities that claim more permissions than they need. In your MCP server manifest, declare only the scopes you actually use:

{
  "name": "my-search-server",
  "permissions": ["network:outbound:api.mysearch.com"]
}

A server that requests broad filesystem or shell access when it only needs to make HTTP calls will score poorly on LLM08.

Control egress

The scanner checks for unexpected outbound network calls. Safe patterns:

  • Use an allowlist of domains your server contacts.
  • Avoid dynamic URL construction from user input.
  • Log all outbound calls (helps buyers audit your server's behaviour).

Pin and audit dependencies

# Before uploading, always run:
npm audit --audit-level=moderate

# Pin exact versions in package.json:
"dependencies": {
  "@modelcontextprotocol/sdk": "1.0.4",
  "zod": "3.22.4"
}

The scanner reads your lockfile. Unpinned ranges (^, ~) are flagged because they allow silent upgrades that could introduce CVEs.

Use safetensors, not pickle

If your MCP server loads a model at runtime, use safetensors format, not pickle. Pickle files can execute arbitrary code on load and will trigger a model-format finding.

Test before upload

# Run the MCP inspector locally:
npx @modelcontextprotocol/inspector server.ts

# Then upload to ai-supply.store
npx ai-supply add . --publish

Capture the local inspector output and include it in your listing as a usage screenshot — buyers appreciate seeing real output.

Checklist before submitting

  • No secrets in any file (including comments and .env.example)
  • All inputs validated with a schema library
  • Dependencies pinned and passing npm audit
  • Egress limited to declared domains
  • Permissions declared at minimum necessary scope
  • No use of eval(), exec(), child_process.exec() with user input
  • README documents all environment variables

For a deeper understanding of what the scanner checks, see how security scanning works.