Use with caution. Address findings before production.
Scanned 5/3/2026, 6:41:35 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
3
high
12
medium
0
low
MCP Server Information
Findings
This package has a C-grade security rating with 3 high-severity vulnerabilities, primarily involving insecure deserialization and ANSI escape injection risks that could enable code execution or terminal manipulation attacks. The 12 medium-severity findings related to server configuration and resource exhaustion suggest additional hardening is needed before deployment. You should address the high-severity issues and review the server configuration recommendations before installing this package in a production environment.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 15 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
15 of 15 findings
15 findings
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 21 | export function extractDescriptions(openapiFile, endpoints) { |
| 22 | console.log('Extracting descriptions from OpenAPI spec...'); |
| 23 | |
| 24 | const openApiSpec = yaml.load(fs.readFileSync(openapiFile, 'utf8')); |
| 25 | const descriptions = {}; |
| 26 | |
| 27 | endpoints.forEach((endpoint) => { |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
Unsafe deserialization primitive detected. pickle.load(s), yaml.load (without SafeLoader), marshal.load(s), and shelve.open execute arbitrary code when the input is attacker-controlled.
Evidence
| 6 | const endpoints = allEndpoints.filter((endpoint) => !endpoint.disabled); |
| 7 | |
| 8 | const spec = fs.readFileSync(openapiFile, 'utf8'); |
| 9 | const openApiSpec = yaml.load(spec); |
| 10 | |
| 11 | for (const endpoint of endpoints) { |
| 12 | if (!openApiSpec.paths[endpoint.pathPattern]) { |
Remediation
Replace pickle with json/msgpack or a schema-validated format (protobuf, cap'n proto). Use yaml.safe_load instead of yaml.load. Never deserialize data from an untrusted source with these APIs.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 3 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; |
| 4 | import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; |
| 5 | import express, { Request, Response } from 'express'; |
| 6 | import logger, { enableConsoleLogging } from './logger.js'; |
| 7 | import { registerAuthTools } from './auth-tools.js'; |
| 8 | i |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 78 | } |
| 79 | |
| 80 | // Clean up - delete the test event |
| 81 | console.log('\nCleaning up test event...'); |
| 82 | await graphClient.graphRequest(`/me/events/${result.id}`, { |
| 83 | method: 'DELETE', |
| 84 | }); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 82 | await graphClient.graphRequest(`/me/events/${result.id}`, { |
| 83 | method: 'DELETE', |
| 84 | }); |
| 85 | console.log('โ Test event deleted'); |
| 86 | } catch (error) { |
| 87 | console.error('Error creating event:', error.message); |
| 88 | if (error.response) { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 84 | }); |
| 85 | console.log('โ Test event deleted'); |
| 86 | } catch (error) { |
| 87 | console.error('Error creating event:', error.message); |
| 88 | if (error.response) { |
| 89 | console.error('Response:', error.response); |
| 90 | } |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 747 | }); |
| 748 | |
| 749 | const newRequestBodyCount = Object.keys(requestBodies).length; |
| 750 | console.log( |
| 751 | ` Removed ${originalRequestBodyCount - newRequestBodyCount} unused request bodies (from ${originalRequestBodyCount} to ${newRequestBodyCount})` |
| 752 | ); |
| 753 | } |
| 754 | } |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 45 | id: 1, |
| 46 | }; |
| 47 | |
| 48 | console.log('Sending test request:', JSON.stringify(testRequest, null, 2)); |
| 49 | |
| 50 | server.stdin.write(JSON.stringify(testRequest) + '\n'); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 74 | console.log('โ SUCCESS! Event was created on the Specific Calendar!'); |
| 75 | } else { |
| 76 | console.log('โ Event was created but on the wrong calendar'); |
| 77 | console.log('Calendar link:', event['calendar@odata.navigationLink']); |
| 78 | } |
| 79 | |
| 80 | // Clean up - delete the test event |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 23 | console.log(`Downloading OpenAPI specification from ${openapiUrl}`); |
| 24 | |
| 25 | try { |
| 26 | const response = await fetch(openapiUrl); |
| 27 | |
| 28 | if (!response.ok) { |
| 29 | throw new Error(`Failed to download: ${response.status} ${response.statusText}`); |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 715 | 'parse-teams-url', |
| 716 | 'Converts any Teams meeting URL format (short /meet/, full /meetup-join/, or recap ?threadId=) into a standard joinWebUrl. Use this before list-online-meetings when the user provides a recap or short URL.', |
| 717 | { |
| 718 | url: z.string().describe('Teams meeting URL in any format'), |
| 719 | }, |
| 720 | { |
| 721 | title: 'parse-teams-url', |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 14 | node-version: [ 18.x, 20.x, 22.x ] |
| 15 | |
| 16 | steps: |
| 17 | - uses: actions/checkout@v4 |
| 18 | |
| 19 | - name: Use Node.js ${{ matrix.node-version }} |
| 20 | uses: actions/setup-node@v4 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 14 | release: |
| 15 | runs-on: ubuntu-latest |
| 16 | steps: |
| 17 | - uses: actions/checkout@v4 |
| 18 | - uses: actions/setup-node@v4 |
| 19 | with: |
| 20 | node-version: 22 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 15 | runs-on: ubuntu-latest |
| 16 | steps: |
| 17 | - uses: actions/checkout@v4 |
| 18 | - uses: actions/setup-node@v4 |
| 19 | with: |
| 20 | node-version: 22 |
| 21 | cache: 'npm' |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 17 | - uses: actions/checkout@v4 |
| 18 | |
| 19 | - name: Use Node.js ${{ matrix.node-version }} |
| 20 | uses: actions/setup-node@v4 |
| 21 | with: |
| 22 | node-version: ${{ matrix.node-version }} |
| 23 | cache: 'npm' |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc