meritmcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of meritmcp might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/action.yml +120 -0
- package/config/scoring.config.json +9 -0
- package/dist/src/adapter/mcpClient.js +72 -0
- package/dist/src/adapter/mcpClient.js.map +1 -0
- package/dist/src/cli.js +121 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/conformance/stdio-proxy.js +102 -0
- package/dist/src/conformance/stdio-proxy.js.map +1 -0
- package/dist/src/conformance/wrapper.js +139 -0
- package/dist/src/conformance/wrapper.js.map +1 -0
- package/dist/src/functional/asserts.js +42 -0
- package/dist/src/functional/asserts.js.map +1 -0
- package/dist/src/functional/parsers.js +44 -0
- package/dist/src/functional/parsers.js.map +1 -0
- package/dist/src/functional/runner.js +27 -0
- package/dist/src/functional/runner.js.map +1 -0
- package/dist/src/orchestrator.js +96 -0
- package/dist/src/orchestrator.js.map +1 -0
- package/dist/src/report/badge.js +10 -0
- package/dist/src/report/badge.js.map +1 -0
- package/dist/src/report/console.js +100 -0
- package/dist/src/report/console.js.map +1 -0
- package/dist/src/report/prcomment.js +52 -0
- package/dist/src/report/prcomment.js.map +1 -0
- package/dist/src/report/sarif.js +118 -0
- package/dist/src/report/sarif.js.map +1 -0
- package/dist/src/report/score.js +59 -0
- package/dist/src/report/score.js.map +1 -0
- package/dist/src/security/engine.js +27 -0
- package/dist/src/security/engine.js.map +1 -0
- package/dist/src/security/owasp-map.js +56 -0
- package/dist/src/security/owasp-map.js.map +1 -0
- package/dist/src/security/probe/authz.js +17 -0
- package/dist/src/security/probe/authz.js.map +1 -0
- package/dist/src/security/probe/injection.js +79 -0
- package/dist/src/security/probe/injection.js.map +1 -0
- package/dist/src/security/static/deps-osv.js +129 -0
- package/dist/src/security/static/deps-osv.js.map +1 -0
- package/dist/src/security/static/secrets.js +57 -0
- package/dist/src/security/static/secrets.js.map +1 -0
- package/dist/src/security/static/unicode.js +37 -0
- package/dist/src/security/static/unicode.js.map +1 -0
- package/dist/src/snapshot/canonicalize.js +11 -0
- package/dist/src/snapshot/canonicalize.js.map +1 -0
- package/dist/src/snapshot/capture.js +26 -0
- package/dist/src/snapshot/capture.js.map +1 -0
- package/dist/src/snapshot/diff.js +26 -0
- package/dist/src/snapshot/diff.js.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +70 -0
- package/schemas/tests.schema.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 '//e0
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Merit
|
|
2
|
+
|
|
3
|
+
> **The CI quality gate for MCP servers** — one command that runs functional tests, the official MCP conformance suite, and OWASP-MCP-Top-10 security checks, then emits a PASS/FAIL verdict, a 0–100 safety score, SARIF for GitHub code scanning, and a README badge.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/meritmcp/meritmcp)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
> ⚠️ **Status: alpha (0.1.x).** Wraps two fast-moving upstreams (`@modelcontextprotocol/sdk`, `@modelcontextprotocol/conformance`) which are pinned and isolated behind adapters. APIs may change before 1.0.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
MCP servers are shipped fast and "vibe-tested." There's no standard way to answer, in one shot: *is my server functionally correct, spec-conformant, and not leaking secrets or running shell input?* Merit is that one shot — and the bundle is the point: **conformance ≠ correct ≠ secure.** A server can pass the official conformance suite while returning wrong answers and exposing a command-injection tool.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Point it at a stdio server…
|
|
20
|
+
npx meritmcp run --stdio "node dist/server.js"
|
|
21
|
+
|
|
22
|
+
# …or a Streamable HTTP server
|
|
23
|
+
npx meritmcp run --http https://your-host/mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
You'll get a verdict like:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Merit — PASS · Safety score 93/100
|
|
30
|
+
|
|
31
|
+
Functional: 12/12 passed
|
|
32
|
+
Conformance: 8/8 applicable scenarios passed (score 100/100) · 22 N/A skipped
|
|
33
|
+
Security: 1 medium (score 96/100)
|
|
34
|
+
▲ [MCP07:2025] Server accepted an unauthenticated MCP session …
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Exit code is `0` on PASS, `1` on FAIL (gate your CI), `2` on a setup error.
|
|
38
|
+
|
|
39
|
+
## What it checks
|
|
40
|
+
|
|
41
|
+
| Engine | What | Weight |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| **Functional** | Your YAML tests (tool calls + assertions) **and** schema-snapshot drift, incl. description-only "rug-pull" detection | 30% |
|
|
44
|
+
| **Conformance** | The **official** MCP conformance suite, wrapped — never reimplemented. Capability- & transport-aware so a tools-only server isn't punished for unimplemented optional features | 30% |
|
|
45
|
+
| **Security** | OWASP-MCP-Top-10. Static (always on): MCP01 secrets · MCP03a invisible-Unicode poisoning · MCP04 dependency CVEs via [OSV.dev](https://osv.dev). Live probes (**opt-in `--probe`**, only on a server you control): MCP05 command-injection · MCP07 HTTP auth | 40% |
|
|
46
|
+
|
|
47
|
+
> The conformance suite is HTTP-only. For stdio servers, Merit transparently spins up an in-process stdio→HTTP proxy so the official suite can test them.
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
meritmcp run --stdio "<command>" | --http <url> [options]
|
|
53
|
+
--tests <path> YAML functional spec (default: merit.tests.yaml)
|
|
54
|
+
--snapshot <path> schema snapshot file (default: merit.snapshot.json)
|
|
55
|
+
--src <dir> server source dir → enables dependency CVE scanning (OSV)
|
|
56
|
+
--conformance-baseline <path> YAML of expected conformance failures
|
|
57
|
+
--min-score <n> fail if the score is below n
|
|
58
|
+
--sarif <path> write SARIF 2.1.0 (GitHub code scanning)
|
|
59
|
+
--out <path> write the full report JSON (open, reproducible scoring)
|
|
60
|
+
--badge <path> write a shields.io endpoint badge JSON
|
|
61
|
+
--pr-comment <path> write the PR-comment markdown
|
|
62
|
+
--probe run LIVE security probes (side effects! only on servers you control)
|
|
63
|
+
--no-security | --no-conformance
|
|
64
|
+
--json print the raw report JSON
|
|
65
|
+
|
|
66
|
+
meritmcp snapshot --stdio "<command>" --out merit.snapshot.json # capture/refresh the tool surface
|
|
67
|
+
meritmcp init # write a starter merit.tests.yaml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Functional test spec (`merit.tests.yaml`)
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
version: 1
|
|
74
|
+
tests:
|
|
75
|
+
- name: "tool catalog is stable"
|
|
76
|
+
op: list_tools
|
|
77
|
+
assert:
|
|
78
|
+
contains_tools: [search, fetch]
|
|
79
|
+
- name: "add returns the sum"
|
|
80
|
+
op: call_tool
|
|
81
|
+
tool: add
|
|
82
|
+
arguments: { a: 2, b: 3 }
|
|
83
|
+
assert:
|
|
84
|
+
not_error: true
|
|
85
|
+
content_contains: "5"
|
|
86
|
+
structured:
|
|
87
|
+
json_schema: { type: object, required: [sum], properties: { sum: { const: 5 } } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## GitHub Action
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# .github/workflows/meritmcp.yml
|
|
94
|
+
name: Merit
|
|
95
|
+
on: [pull_request]
|
|
96
|
+
permissions:
|
|
97
|
+
contents: read
|
|
98
|
+
security-events: write # upload SARIF
|
|
99
|
+
pull-requests: write # sticky PR comment
|
|
100
|
+
jobs:
|
|
101
|
+
meritmcp:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
- run: npm ci && npm run build
|
|
106
|
+
- uses: meritmcp/meritmcp@v0 # or: meritmcp/meritmcp@<sha>
|
|
107
|
+
with:
|
|
108
|
+
command: "node dist/server.js" # or: url: https://your-host/mcp
|
|
109
|
+
src: "." # enable dependency CVE scanning
|
|
110
|
+
min-score: "70"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The Action runs Merit, uploads SARIF to the **Security** tab, and posts a sticky PR comment with the verdict. Outputs: `score`, `verdict`.
|
|
114
|
+
|
|
115
|
+
## The badge
|
|
116
|
+
|
|
117
|
+
`meritmcp run … --badge badge.json` writes a [shields.io endpoint](https://shields.io/endpoint) file. Publish it (gh-pages / a Gist) and embed:
|
|
118
|
+
|
|
119
|
+
```markdown
|
|
120
|
+

|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Scoring (open + reproducible)
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
score = 0.30·functional + 0.30·conformance + 0.40·security
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- Engines that don't run have their weight redistributed across the rest.
|
|
130
|
+
- **Security cap:** any *high-confidence* Critical/High finding caps the total at **49 and forces FAIL** — a pretty score can't hide a real hole.
|
|
131
|
+
- A description-only schema change (rug-pull) costs 15 points.
|
|
132
|
+
- Every weight lives in [`config/scoring.config.json`](./config/scoring.config.json) and every finding's contribution is in `merit.json`, so anyone can recompute the score by hand.
|
|
133
|
+
|
|
134
|
+
Low-confidence/heuristic findings are reported as notes — they never hard-FAIL. Detection methodology and confidence per OWASP risk: [`04-SECURITY-OWASP.md`](./04-SECURITY-OWASP.md).
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm install
|
|
140
|
+
npm run demo:pass # green PASS against a clean fixture
|
|
141
|
+
npm run demo:fail # red FAIL against a deliberately broken+insecure fixture
|
|
142
|
+
npm test # unit tests
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Design docs: [`01-DESIGN.md`](./01-DESIGN.md) · [`03-ARCHITECTURE.md`](./03-ARCHITECTURE.md) · [`04-SECURITY-OWASP.md`](./04-SECURITY-OWASP.md).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](./LICENSE)
|
package/action.yml
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: "Merit"
|
|
2
|
+
description: "The CI quality gate for MCP servers — functional + official conformance + OWASP-MCP-Top-10 security, with a SARIF report and a score badge."
|
|
3
|
+
branding:
|
|
4
|
+
icon: "shield"
|
|
5
|
+
color: "green"
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
command:
|
|
9
|
+
description: "stdio command to launch the MCP server (e.g. 'node dist/server.js'). Use this OR url."
|
|
10
|
+
required: false
|
|
11
|
+
url:
|
|
12
|
+
description: "Streamable HTTP MCP server URL. Use this OR command."
|
|
13
|
+
required: false
|
|
14
|
+
tests:
|
|
15
|
+
description: "Path to the YAML functional test spec."
|
|
16
|
+
required: false
|
|
17
|
+
default: "merit.tests.yaml"
|
|
18
|
+
src:
|
|
19
|
+
description: "Server source dir (enables dependency CVE scanning via OSV)."
|
|
20
|
+
required: false
|
|
21
|
+
snapshot:
|
|
22
|
+
description: "Schema snapshot file."
|
|
23
|
+
required: false
|
|
24
|
+
default: "merit.snapshot.json"
|
|
25
|
+
min-score:
|
|
26
|
+
description: "Fail the job if the score is below this (0-100)."
|
|
27
|
+
required: false
|
|
28
|
+
default: "0"
|
|
29
|
+
conformance:
|
|
30
|
+
description: "Run the official MCP conformance suite ('true'/'false')."
|
|
31
|
+
required: false
|
|
32
|
+
default: "true"
|
|
33
|
+
conformance-baseline:
|
|
34
|
+
description: "YAML of expected conformance failures (baseline)."
|
|
35
|
+
required: false
|
|
36
|
+
version:
|
|
37
|
+
description: "meritmcp npm version / dist-tag to run."
|
|
38
|
+
required: false
|
|
39
|
+
default: "latest"
|
|
40
|
+
node-version:
|
|
41
|
+
description: "Node version to set up."
|
|
42
|
+
required: false
|
|
43
|
+
default: "20"
|
|
44
|
+
comment-pr:
|
|
45
|
+
description: "Post a sticky PR comment with the results ('true'/'false')."
|
|
46
|
+
required: false
|
|
47
|
+
default: "true"
|
|
48
|
+
|
|
49
|
+
outputs:
|
|
50
|
+
score:
|
|
51
|
+
description: "The 0-100 safety score."
|
|
52
|
+
value: ${{ steps.meritmcp.outputs.score }}
|
|
53
|
+
verdict:
|
|
54
|
+
description: "PASS or FAIL."
|
|
55
|
+
value: ${{ steps.meritmcp.outputs.verdict }}
|
|
56
|
+
|
|
57
|
+
runs:
|
|
58
|
+
using: "composite"
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/setup-node@v4
|
|
61
|
+
with:
|
|
62
|
+
node-version: ${{ inputs.node-version }}
|
|
63
|
+
|
|
64
|
+
- id: meritmcp
|
|
65
|
+
shell: bash
|
|
66
|
+
env:
|
|
67
|
+
IN_COMMAND: ${{ inputs.command }}
|
|
68
|
+
IN_URL: ${{ inputs.url }}
|
|
69
|
+
IN_TESTS: ${{ inputs.tests }}
|
|
70
|
+
IN_SRC: ${{ inputs.src }}
|
|
71
|
+
IN_SNAPSHOT: ${{ inputs.snapshot }}
|
|
72
|
+
IN_MIN_SCORE: ${{ inputs.min-score }}
|
|
73
|
+
IN_CONFORMANCE: ${{ inputs.conformance }}
|
|
74
|
+
IN_BASELINE: ${{ inputs.conformance-baseline }}
|
|
75
|
+
IN_VERSION: ${{ inputs.version }}
|
|
76
|
+
run: |
|
|
77
|
+
set -u
|
|
78
|
+
ARGS=(run --tests "$IN_TESTS" --snapshot "$IN_SNAPSHOT" --min-score "$IN_MIN_SCORE"
|
|
79
|
+
--sarif merit.sarif --out merit.json --badge merit-badge.json --pr-comment merit-pr.md)
|
|
80
|
+
[ -n "$IN_COMMAND" ] && ARGS+=(--stdio "$IN_COMMAND")
|
|
81
|
+
[ -n "$IN_URL" ] && ARGS+=(--http "$IN_URL")
|
|
82
|
+
[ -n "$IN_SRC" ] && ARGS+=(--src "$IN_SRC")
|
|
83
|
+
[ "$IN_CONFORMANCE" = "false" ] && ARGS+=(--no-conformance)
|
|
84
|
+
[ -n "$IN_BASELINE" ] && ARGS+=(--conformance-baseline "$IN_BASELINE")
|
|
85
|
+
|
|
86
|
+
set +e
|
|
87
|
+
npx -y "meritmcp@${IN_VERSION}" "${ARGS[@]}"
|
|
88
|
+
CODE=$?
|
|
89
|
+
set -e
|
|
90
|
+
|
|
91
|
+
if [ -f merit.json ]; then
|
|
92
|
+
echo "score=$(node -p "require('./merit.json').score")" >> "$GITHUB_OUTPUT"
|
|
93
|
+
echo "verdict=$(node -p "require('./merit.json').verdict")" >> "$GITHUB_OUTPUT"
|
|
94
|
+
fi
|
|
95
|
+
exit $CODE
|
|
96
|
+
|
|
97
|
+
- name: Upload SARIF to code scanning
|
|
98
|
+
if: ${{ always() && hashFiles('merit.sarif') != '' }}
|
|
99
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
100
|
+
with:
|
|
101
|
+
sarif_file: merit.sarif
|
|
102
|
+
|
|
103
|
+
- name: Sticky PR comment
|
|
104
|
+
if: ${{ always() && github.event_name == 'pull_request' && inputs.comment-pr == 'true' }}
|
|
105
|
+
uses: actions/github-script@v7
|
|
106
|
+
with:
|
|
107
|
+
script: |
|
|
108
|
+
const fs = require('fs');
|
|
109
|
+
if (!fs.existsSync('merit-pr.md')) return;
|
|
110
|
+
const body = fs.readFileSync('merit-pr.md', 'utf8');
|
|
111
|
+
const marker = '<!-- merit-report -->';
|
|
112
|
+
const { owner, repo } = context.repo;
|
|
113
|
+
const issue_number = context.issue.number;
|
|
114
|
+
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number });
|
|
115
|
+
const existing = comments.find((c) => c.body && c.body.includes(marker));
|
|
116
|
+
if (existing) {
|
|
117
|
+
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
|
118
|
+
} else {
|
|
119
|
+
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Open, reproducible scoring weights. Mirrors DEFAULT_SCORING in src/report/score.ts. Score = 0.30*Functional + 0.30*Conformance + 0.40*Security; engines that don't run have their weight redistributed.",
|
|
3
|
+
"weights": { "functional": 0.3, "conformance": 0.3, "security": 0.4 },
|
|
4
|
+
"securityPenalties": { "critical": 40, "high": 20, "medium": 8, "low": 3 },
|
|
5
|
+
"confidenceMultiplier": { "high": 1.0, "medium": 0.5, "low": 0.2 },
|
|
6
|
+
"rugPullPenalty": 15,
|
|
7
|
+
"capScore": 49,
|
|
8
|
+
"bands": { "brightgreen": 85, "green": 70, "yellow": 50, "red": 30 }
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// THE single SDK chokepoint. Every @modelcontextprotocol/sdk call lives here so
|
|
2
|
+
// the coming v2 package split is a one-file migration (see 03-ARCHITECTURE.md).
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
export class McpClient {
|
|
7
|
+
client;
|
|
8
|
+
constructor(name = "merit", version = "0.1.0") {
|
|
9
|
+
this.client = new Client({ name, version }, { capabilities: {} });
|
|
10
|
+
}
|
|
11
|
+
async connect(opts) {
|
|
12
|
+
if (opts.transport === "http") {
|
|
13
|
+
if (!opts.url)
|
|
14
|
+
throw new Error('HTTP transport requires --http <url>');
|
|
15
|
+
await this.client.connect(new StreamableHTTPClientTransport(new URL(opts.url)));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!opts.command)
|
|
19
|
+
throw new Error('stdio transport requires --stdio "<command>"');
|
|
20
|
+
const { command, args } = splitCommand(opts.command);
|
|
21
|
+
await this.client.connect(new StdioClientTransport({ command, args, env: { ...currentEnv(), ...(opts.env ?? {}) } }));
|
|
22
|
+
}
|
|
23
|
+
/** Enumerate all tools, following pagination cursors. */
|
|
24
|
+
async listTools() {
|
|
25
|
+
const out = [];
|
|
26
|
+
let cursor;
|
|
27
|
+
do {
|
|
28
|
+
const page = (await this.client.listTools(cursor ? { cursor } : undefined));
|
|
29
|
+
out.push(...(page.tools ?? []));
|
|
30
|
+
cursor = page.nextCursor;
|
|
31
|
+
} while (cursor);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
async callTool(name, args) {
|
|
35
|
+
const res = (await this.client.callTool({ name, arguments: args }));
|
|
36
|
+
const text = (res.content ?? [])
|
|
37
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
38
|
+
.map((c) => c.text)
|
|
39
|
+
.join("\n");
|
|
40
|
+
return { text, isError: Boolean(res.isError), structured: res.structuredContent, raw: res };
|
|
41
|
+
}
|
|
42
|
+
/** Server capabilities negotiated during initialize (available after connect). */
|
|
43
|
+
serverCapabilities() {
|
|
44
|
+
return (this.client.getServerCapabilities() ?? {});
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
try {
|
|
48
|
+
await this.client.close();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* server may already be gone; ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Split a shell-ish command string into command + args, respecting quotes. */
|
|
56
|
+
export function splitCommand(cmd) {
|
|
57
|
+
const matches = cmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
58
|
+
const parts = matches.map((s) => s.replace(/^["']|["']$/g, ""));
|
|
59
|
+
const [command, ...args] = parts;
|
|
60
|
+
if (!command)
|
|
61
|
+
throw new Error(`could not parse command: ${cmd}`);
|
|
62
|
+
return { command, args };
|
|
63
|
+
}
|
|
64
|
+
/** process.env with undefined values dropped (StdioClientTransport wants Record<string,string>). */
|
|
65
|
+
export function currentEnv() {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(process.env))
|
|
68
|
+
if (typeof v === "string")
|
|
69
|
+
out[k] = v;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=mcpClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpClient.js","sourceRoot":"","sources":["../../../src/adapter/mcpClient.ts"],"names":[],"mappings":"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"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { run } from "./orchestrator.js";
|
|
5
|
+
import { McpClient } from "./adapter/mcpClient.js";
|
|
6
|
+
import { capture } from "./snapshot/capture.js";
|
|
7
|
+
import { printReport } from "./report/console.js";
|
|
8
|
+
import { toSarif } from "./report/sarif.js";
|
|
9
|
+
import { toBadge } from "./report/badge.js";
|
|
10
|
+
import { toMarkdown } from "./report/prcomment.js";
|
|
11
|
+
import { DEFAULT_SCORING } from "./report/score.js";
|
|
12
|
+
const STARTER_TESTS = `version: 1
|
|
13
|
+
# Merit functional tests. Point the CLI at your server with --stdio / --http.
|
|
14
|
+
tests:
|
|
15
|
+
- name: "tool catalog is stable"
|
|
16
|
+
op: list_tools
|
|
17
|
+
assert:
|
|
18
|
+
contains_tools: [] # e.g. [search, fetch]
|
|
19
|
+
# - name: "add returns the sum"
|
|
20
|
+
# op: call_tool
|
|
21
|
+
# tool: add
|
|
22
|
+
# arguments: { a: 2, b: 3 }
|
|
23
|
+
# assert:
|
|
24
|
+
# not_error: true
|
|
25
|
+
# content_contains: "5"
|
|
26
|
+
`;
|
|
27
|
+
const program = new Command();
|
|
28
|
+
program.name("merit").description("The CI quality gate for MCP servers").version("0.1.0");
|
|
29
|
+
function connectOpts(opts) {
|
|
30
|
+
return opts.http ? { transport: "http", url: opts.http } : { transport: "stdio", command: opts.stdio };
|
|
31
|
+
}
|
|
32
|
+
program
|
|
33
|
+
.command("run")
|
|
34
|
+
.description("Connect to an MCP server and run the quality gate: functional tests + schema-drift + official conformance + OWASP-MCP-Top-10 security.")
|
|
35
|
+
.option("--stdio <command>", 'launch a stdio MCP server, e.g. "node dist/server.js"')
|
|
36
|
+
.option("--http <url>", "connect to a Streamable HTTP MCP server URL")
|
|
37
|
+
.option("--tests <path>", "YAML functional test spec", "merit.tests.yaml")
|
|
38
|
+
.option("--snapshot <path>", "schema snapshot file", "merit.snapshot.json")
|
|
39
|
+
.option("--src <dir>", "server source dir (enables dependency CVE scanning via OSV)")
|
|
40
|
+
.option("--no-security", "skip the OWASP-MCP-Top-10 security checks")
|
|
41
|
+
.option("--probe", "run LIVE security probes — sends canary payloads to the server's tools (may trigger real tool actions; only use on a server you control). Off by default.", false)
|
|
42
|
+
.option("--no-conformance", "skip the official MCP conformance suite")
|
|
43
|
+
.option("--conformance-baseline <path>", "YAML of expected conformance failures (baseline)")
|
|
44
|
+
.option("--min-score <n>", "fail if the score is below this", "0")
|
|
45
|
+
.option("--sarif <path>", "write SARIF 2.1.0 (for GitHub code scanning)")
|
|
46
|
+
.option("--out <path>", "write the full report JSON (open, reproducible scoring)")
|
|
47
|
+
.option("--badge <path>", "write a shields.io endpoint badge JSON")
|
|
48
|
+
.option("--pr-comment <path>", "write the PR-comment markdown")
|
|
49
|
+
.option("--json", "print the raw report as JSON instead of the console verdict", false)
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
if (!opts.stdio && !opts.http) {
|
|
52
|
+
console.error('✖ provide --stdio "<command>" or --http <url>');
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const report = await run({
|
|
57
|
+
connect: connectOpts(opts),
|
|
58
|
+
testsPath: opts.tests,
|
|
59
|
+
snapshotPath: opts.snapshot,
|
|
60
|
+
srcDir: opts.src,
|
|
61
|
+
security: opts.security,
|
|
62
|
+
probe: opts.probe,
|
|
63
|
+
conformance: opts.conformance,
|
|
64
|
+
conformanceBaseline: opts.conformanceBaseline,
|
|
65
|
+
});
|
|
66
|
+
if (opts.sarif)
|
|
67
|
+
writeFileSync(opts.sarif, JSON.stringify(toSarif(report), null, 2));
|
|
68
|
+
if (opts.badge)
|
|
69
|
+
writeFileSync(opts.badge, JSON.stringify(toBadge(report), null, 2));
|
|
70
|
+
if (opts.prComment)
|
|
71
|
+
writeFileSync(opts.prComment, toMarkdown(report));
|
|
72
|
+
if (opts.out)
|
|
73
|
+
writeFileSync(opts.out, JSON.stringify({ ...report, scoring: DEFAULT_SCORING, generatedAt: new Date().toISOString() }, null, 2));
|
|
74
|
+
if (opts.json)
|
|
75
|
+
console.log(JSON.stringify(report, null, 2));
|
|
76
|
+
else
|
|
77
|
+
printReport(report);
|
|
78
|
+
const ok = report.verdict === "PASS" && report.score >= Number(opts.minScore);
|
|
79
|
+
process.exit(ok ? 0 : 1);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(`✖ ${e.message}`);
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
program
|
|
87
|
+
.command("snapshot")
|
|
88
|
+
.description("Capture/refresh the schema snapshot of a server's tool surface.")
|
|
89
|
+
.option("--stdio <command>", "launch a stdio MCP server")
|
|
90
|
+
.option("--http <url>", "connect to a Streamable HTTP MCP server URL")
|
|
91
|
+
.option("--out <path>", "snapshot output file", "merit.snapshot.json")
|
|
92
|
+
.action(async (opts) => {
|
|
93
|
+
if (!opts.stdio && !opts.http) {
|
|
94
|
+
console.error("✖ provide --stdio or --http");
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
const client = new McpClient();
|
|
98
|
+
try {
|
|
99
|
+
await client.connect(connectOpts(opts));
|
|
100
|
+
const snap = capture(await client.listTools());
|
|
101
|
+
writeFileSync(opts.out, JSON.stringify(snap, null, 2));
|
|
102
|
+
console.log(`✔ wrote ${opts.out} (rootHash ${snap.rootHash.slice(0, 12)}…, ${Object.keys(snap.tools).length} tools)`);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await client.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
program
|
|
109
|
+
.command("init")
|
|
110
|
+
.description("Write a starter merit.tests.yaml.")
|
|
111
|
+
.option("--out <path>", "output path", "merit.tests.yaml")
|
|
112
|
+
.action((opts) => {
|
|
113
|
+
if (existsSync(opts.out)) {
|
|
114
|
+
console.error(`✖ ${opts.out} already exists`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
writeFileSync(opts.out, STARTER_TESTS);
|
|
118
|
+
console.log(`✔ wrote ${opts.out} — edit it, then: merit run --stdio "node dist/server.js"`);
|
|
119
|
+
});
|
|
120
|
+
program.parseAsync();
|
|
121
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// stdio→HTTP proxy. The official conformance suite is HTTP-only (--url, no --stdio),
|
|
2
|
+
// so to test a stdio server we expose it over Streamable HTTP here. This is a transparent
|
|
3
|
+
// JSON-RPC relay: an HTTP server transport (facing conformance) bridged to a fresh stdio
|
|
4
|
+
// child per session (the server-under-test). Messages are forwarded verbatim — no parsing.
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
|
+
import { splitCommand, currentEnv } from "../adapter/mcpClient.js";
|
|
10
|
+
export async function startStdioHttpProxy(commandString, path = "/mcp") {
|
|
11
|
+
const { command, args } = splitCommand(commandString);
|
|
12
|
+
const env = currentEnv();
|
|
13
|
+
const sessions = new Map();
|
|
14
|
+
async function newSession() {
|
|
15
|
+
const child = new StdioClientTransport({ command, args, env });
|
|
16
|
+
const http = new StreamableHTTPServerTransport({
|
|
17
|
+
sessionIdGenerator: () => randomUUID(),
|
|
18
|
+
onsessioninitialized: (sid) => {
|
|
19
|
+
sessions.set(sid, { http, close });
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
// Idempotent teardown — both transports' onclose route here, and so does proxy.close(),
|
|
23
|
+
// so the underlying handles are only closed once (avoids libuv double-close on Windows).
|
|
24
|
+
let closed = false;
|
|
25
|
+
async function close() {
|
|
26
|
+
if (closed)
|
|
27
|
+
return;
|
|
28
|
+
closed = true;
|
|
29
|
+
await http.close().catch(() => { });
|
|
30
|
+
await child.close().catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
// Transparent bidirectional relay (verbatim JSON-RPC).
|
|
33
|
+
http.onmessage = (msg) => void child.send(msg).catch(() => { });
|
|
34
|
+
child.onmessage = (msg) => void http.send(msg).catch(() => { });
|
|
35
|
+
http.onclose = () => void close();
|
|
36
|
+
child.onclose = () => void close();
|
|
37
|
+
await child.start();
|
|
38
|
+
await http.start();
|
|
39
|
+
return http;
|
|
40
|
+
}
|
|
41
|
+
const server = createServer(async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
if (!req.url || !req.url.startsWith(path)) {
|
|
44
|
+
res.statusCode = 404;
|
|
45
|
+
res.end();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const sid = req.headers["mcp-session-id"];
|
|
49
|
+
let transport = sid ? sessions.get(sid)?.http : undefined;
|
|
50
|
+
const body = await readBody(req);
|
|
51
|
+
if (!transport) {
|
|
52
|
+
if (isInitialize(body)) {
|
|
53
|
+
transport = await newSession();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
res.statusCode = 400;
|
|
57
|
+
res.setHeader("content-type", "application/json");
|
|
58
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "No valid session" }, id: null }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await transport.handleRequest(req, res, body);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
if (!res.headersSent) {
|
|
66
|
+
res.statusCode = 500;
|
|
67
|
+
res.end();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
72
|
+
const port = server.address().port;
|
|
73
|
+
return {
|
|
74
|
+
url: `http://127.0.0.1:${port}${path}`,
|
|
75
|
+
close: async () => {
|
|
76
|
+
for (const s of sessions.values())
|
|
77
|
+
await s.close();
|
|
78
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isInitialize(body) {
|
|
83
|
+
const first = Array.isArray(body) ? body[0] : body;
|
|
84
|
+
return Boolean(first) && typeof first === "object" && first.method === "initialize";
|
|
85
|
+
}
|
|
86
|
+
async function readBody(req) {
|
|
87
|
+
if (req.method !== "POST")
|
|
88
|
+
return undefined;
|
|
89
|
+
const chunks = [];
|
|
90
|
+
for await (const c of req)
|
|
91
|
+
chunks.push(c);
|
|
92
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
93
|
+
if (!raw)
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return raw;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=stdio-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-proxy.js","sourceRoot":"","sources":["../../../src/conformance/stdio-proxy.ts"],"names":[],"mappings":"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"}
|