Asset Management·OWASP API9·Production Hardening

MCP server improper assets management: debug tools and undocumented endpoints in production

OWASP API9:2023 (Improper Inventory Management) covers undocumented API endpoints, debug interfaces, and staging versions accessible in production. MCP servers face an equivalent problem: debug and inspection tools added during development are often never removed before production deployment. These tools are invisible to documentation and security reviews but fully accessible to any authenticated caller — and sometimes to unauthenticated callers when tool-level authorization is weak.

The development tool attack surface

Development environments benefit from diagnostic tools: debug_list_sessions to inspect active connections, inspect_cache to view internal state, eval_expression to test argument processing, dump_config to verify environment variables are loaded correctly. These tools are legitimate during development. In production, they are high-value attack targets:

A server with a grade of C or below has a roughly 40% chance (based on SkillAudit scan corpus) of having at least one debug-category tool registered in its production tool list. These tools are often present because the developer added them once and the codebase was never reviewed for production readiness before deploy.

Pattern 1: environment-specific tool registration

Gate debug and inspection tool registration on environment. Production builds should never register these tools, and the registration code should be structured so that the gate is enforced at the module boundary, not conditionally inside the handler:

// tools/debug.ts — entire module is conditional
if (process.env.NODE_ENV !== 'production') {
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
    if (req.params.name === 'debug_list_sessions') {
      return { content: [{ type: 'text', text: JSON.stringify(sessionStore.list()) }] };
    }
    if (req.params.name === 'inspect_cache') {
      return { content: [{ type: 'text', text: JSON.stringify(cache.dump()) }] };
    }
  });
}

// Better: explicit registration function only called in non-prod
export function registerDebugTools(server: McpServer) {
  if (process.env.NODE_ENV === 'production') return; // early exit
  server.tool('debug_list_sessions', {}, handleDebugListSessions);
  server.tool('inspect_cache', {}, handleInspectCache);
}

The key architectural principle: debug tool registration code is never in the main entry point. It lives in a separate module with an explicit environment guard at the top. A code reviewer can verify the guard is present without reading every handler.

Pattern 2: production tool manifest with CI validation

Maintain a tools.production.json manifest listing every tool that should be registered in production. CI runs a validation step that compares the running server's registered tools against the manifest:

// tools.production.json
{
  "allowed": [
    "read_document",
    "list_documents",
    "create_document",
    "update_document",
    "delete_document",
    "search_documents"
  ]
}

// In CI: start server, fetch tool list, compare to manifest
const server = await startServer({ env: 'production' });
const registered = await server.listTools();
const manifest = JSON.parse(fs.readFileSync('tools.production.json', 'utf8'));

const unexpected = registered.filter(t => !manifest.allowed.includes(t.name));
if (unexpected.length > 0) {
  console.error('FAIL: unexpected tools registered in production build:', unexpected);
  process.exit(1);
}

This CI gate means a debug tool can never accidentally ship to production without the manifest being updated — which is a deliberate, code-reviewed action, not an oversight.

Pattern 3: dead tool detection

Tools that are registered but never invoked in production are either unnecessary or forgotten. A dead-tool detection step in weekly CI reviews logs to find tools with zero invocations in the past 30 days:

# Query structured tool call logs for invocations per tool (last 30d)
# Log format: {"ts":"...","tool":"...","caller":"...","outcome":"..."}
jq -r '.tool' logs/tool-calls.jsonl \
  | sort | uniq -c | sort -rn \
  | awk '{print $2, $1}' > /tmp/tool-usage.txt

# Check for registered tools with no usage
while read tool; do
  if ! grep -q "^$tool " /tmp/tool-usage.txt; then
    echo "WARNING: tool '$tool' registered but has 0 invocations in last 30d"
  fi
done < <(jq -r '.allowed[]' tools.production.json)

Zero-invocation tools are either dead code (should be removed) or undocumented capabilities (should be documented and reviewed). Neither outcome is neutral — the detection step forces a decision.

Pattern 4: version-controlled tool API with semantic versioning

Treat the tool list as a versioned API. Breaking changes — renaming tools, removing tools, changing argument schemas — should require a version bump and a migration guide. Undocumented tools that were never in the changelog are a signal of improper asset management:

// tools-changelog.md entry for each release
// v2.1.0
// Added: create_document (documents:write scope required)
// Added: bulk_delete_documents (documents:delete scope, admin only)
// Deprecated: batch_create (use create_document; will be removed in v3.0.0)
// Removed: debug_echo (was development-only, should never have been in v1.x)

If a tool appears in the registered list but not in the changelog, it is an undocumented endpoint — the OWASP API9 definition of improper asset management. The changelog forces documentation of every tool's lifecycle.

Pattern 5: tool discovery audit on deploy

After each production deployment, run a tool discovery audit that compares the new server's registered tools against the previous deployment's manifest. Any net-new tools trigger an alert for security review:

// deploy-hook.sh
PREV_TOOLS=$(cat .deploy/last-tool-manifest.json)
NEW_TOOLS=$(curl -s http://localhost:3000/tools | jq '[.[].name] | sort')

ADDED=$(jq -n --argjson p "$PREV_TOOLS" --argjson n "$NEW_TOOLS" \
  '[$n[] | select(. as $t | $p | index($t) | not)]')

if [ "$(echo $ADDED | jq length)" -gt 0 ]; then
  echo "DEPLOY ALERT: new tools registered: $ADDED"
  # Notify security team, block if in strict mode
fi

echo $NEW_TOOLS > .deploy/last-tool-manifest.json

This continuous audit means that each deployment's tool surface is explicitly tracked. A debug tool added during a hotfix and accidentally left in a production deploy is caught at the next deployment rather than discovered months later by an attacker.

What SkillAudit checks for improper assets management

SkillAudit scans for improper asset management by flagging tool names and registration patterns that indicate development tools in production code:

Improper asset management findings map to the Maintenance sub-score in SkillAudit reports. A server with debug tools in its production build receives a HIGH finding that prevents an A grade. See the security policy template for how to document the tool lifecycle in a way that satisfies SkillAudit's Maintenance check.

Audit your MCP server for debug tools in production

SkillAudit scans for debug tool name patterns, eval-based handlers, and missing tool manifests in your production codebase. Get your grade and prioritized fixes in under 2 minutes.

Run free scan →