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:
debug_list_sessions— exposes all active session tokens and user identitiesinspect_cache— may expose recently-fetched sensitive data from upstream APIseval_expression— code execution if the expression evaluator is backed byeval()orvm.runInContext()dump_config— directly exposes environment variables including API keys and secrets
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:
- Debug tool name patterns: tools named
debug_*,inspect_*,eval_*,dump_*,test_*,dev_*registered without an explicitNODE_ENV !== 'production'guard - Missing tool manifest: no
tools.production.jsonor equivalent allowlist; tool set is defined only by the registration code with no external verification point - Eval-based tools: tool handlers that invoke
eval(),vm.runInContext(),new Function(), or equivalent with any argument derived from tool input — these are particularly dangerous if left accessible - Config-dump tools: tools that return process environment variables, configuration objects, or server state without explicit admin-only scope restrictions
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 →