ninjaeval-mcp 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,2 @@
1
+ """NinjaEval MCP server: test an AI change from a coding agent before calling it done."""
2
+ __version__ = "0.1.0"
ninjaeval_mcp/api.py ADDED
@@ -0,0 +1,234 @@
1
+ """NinjaEval API client and the compact summaries the MCP tools return. Standard library only, so it can be tested
2
+ without the MCP package."""
3
+ import json
4
+ import os
5
+ import ssl
6
+ import time
7
+ import urllib.error
8
+ import urllib.parse
9
+ import urllib.request
10
+
11
+ DEFAULT_API = "https://api.ninjaeval.com/api/v1/eng"
12
+ DEFAULT_APP = "https://ninjaeval.com"
13
+ PASS = "PASS"
14
+ FAILING = ("regressed", "errored")
15
+
16
+
17
+ def _tls():
18
+ """certifi's CA bundle when installed (mcp brings it via httpx): python.org builds on macOS have none by default."""
19
+ try:
20
+ import certifi
21
+ return ssl.create_default_context(cafile=certifi.where())
22
+ except ImportError:
23
+ return ssl.create_default_context()
24
+
25
+
26
+ class NinjaEvalError(RuntimeError):
27
+ pass
28
+
29
+
30
+ def _key_from_env():
31
+ key = os.environ.get("NINJAEVAL_API_KEY", "").strip()
32
+ path = os.environ.get("NINJAEVAL_API_KEY_FILE", "").strip()
33
+ if not key and path:
34
+ with open(os.path.expanduser(path)) as f:
35
+ key = f.read().strip()
36
+ return key
37
+
38
+
39
+ class Client:
40
+ """One workspace, through its API key (`efk_…`, from ⚙ Workspace & API keys)."""
41
+
42
+ def __init__(self, key=None, api=None, app=None, sleep=time.sleep, clock=time.monotonic):
43
+ self.key = key if key is not None else _key_from_env()
44
+ self.api = (api or os.environ.get("NINJAEVAL_API_URL") or DEFAULT_API).rstrip("/")
45
+ self.app = (app or os.environ.get("NINJAEVAL_APP_URL") or DEFAULT_APP).rstrip("/")
46
+ self.sleep, self.clock = sleep, clock
47
+
48
+ # ── HTTP ──
49
+ def call(self, method, path, body=None, text=False):
50
+ if not self.key:
51
+ raise NinjaEvalError("No API key: set NINJAEVAL_API_KEY (or NINJAEVAL_API_KEY_FILE) to a workspace key "
52
+ "from NinjaEval → ⚙ Workspace & API keys.")
53
+ req = urllib.request.Request(self.api + path, method=method,
54
+ data=json.dumps(body).encode() if body is not None else None,
55
+ headers={"Authorization": f"Bearer {self.key}", "Content-Type": "application/json",
56
+ "User-Agent": "ninjaeval-mcp"})
57
+ try:
58
+ with urllib.request.urlopen(req, timeout=60, context=_tls()) as r:
59
+ raw = r.read().decode()
60
+ except urllib.error.HTTPError as e:
61
+ detail = e.read().decode(errors="replace")
62
+ try:
63
+ detail = json.loads(detail).get("detail", detail)
64
+ except ValueError:
65
+ pass
66
+ raise NinjaEvalError(f"NinjaEval API {method} {path}: {e.code} {detail}") from None
67
+ except urllib.error.URLError as e:
68
+ raise NinjaEvalError(f"Can't reach NinjaEval at {self.api}: {e.reason}") from None
69
+ return raw if text else (json.loads(raw) if raw else None)
70
+
71
+ # ── reads ──
72
+ def me(self):
73
+ m = self.call("GET", "/me")
74
+ return {k: m.get(k) for k in ("workspace", "role", "can_run", "can_manage", "workspace_limits") if k in m}
75
+
76
+ def projects(self):
77
+ return [{"project_id": p["project_id"], "name": p.get("name"), "run_mode": p.get("run_mode"),
78
+ "description": p.get("description") or ""} for p in self.call("GET", "/projects")["projects"]]
79
+
80
+ def project(self, project_id):
81
+ v = self.call("GET", f"/projects/{q(project_id)}")
82
+ prod = v.get("production") or {}
83
+ return {
84
+ "project_id": project_id, "name": v["project"].get("name"), "run_mode": v["project"].get("run_mode"),
85
+ "gate": v["project"].get("gate"),
86
+ "production": {k: prod.get(k) for k in ("label", "provider", "model", "params", "system_prompt", "tools", "code_sha", "note")}
87
+ if prod else None,
88
+ "suites": [{"dataset_id": d["dataset_id"], "name": d.get("name"), "cases": d.get("item_count")} for d in v.get("datasets") or []],
89
+ "ready_to_test": all((v.get("setup") or {}).values()),
90
+ "setup": v.get("setup"),
91
+ "recent_change_tests": [{"test_id": t["test_id"], "candidate": t.get("candidate_label"), "baseline": t.get("baseline_label"),
92
+ "status": t.get("status"), "complete": t.get("complete"), "created_at": t.get("created_at"),
93
+ "url": self.test_url(t["test_id"])} for t in (v.get("change_tests") or [])[:5]],
94
+ }
95
+
96
+ def test(self, test_id):
97
+ return self.call("GET", f"/change-tests/{q(test_id)}")
98
+
99
+ def report(self, test_id):
100
+ return self.call("GET", f"/change-tests/{q(test_id)}/report.md", text=True)
101
+
102
+ def test_url(self, test_id):
103
+ return f"{self.app}/#/change/{test_id}"
104
+
105
+ # ── writes ──
106
+ def start_change_test(self, project_id, **changes):
107
+ body = {k: v for k, v in changes.items() if v is not None}
108
+ return self.call("POST", f"/projects/{q(project_id)}/test-change", body)
109
+
110
+ def analyze(self, test_id, item_ids, runs=3):
111
+ return self.call("POST", f"/change-tests/{q(test_id)}/analyze", {"item_ids": list(item_ids), "runs": runs})
112
+
113
+ # ── waiting ──
114
+ def wait(self, test_id, timeout=600, poll=5, diag_grace=120, item_ids=None):
115
+ """Poll until the runs finish and the diagnoses of the failing cases (or `item_ids`) settle, or the timeout.
116
+ A diagnosis that hasn't started `diag_grace` seconds after the runs finish isn't waited for."""
117
+ start = self.clock()
118
+ finished_at = None
119
+ while True:
120
+ t = self.test(test_id)
121
+ if runs_done(t):
122
+ finished_at = finished_at if finished_at is not None else self.clock()
123
+ waiting = diagnoses_pending(t, item_ids)
124
+ if not waiting or (not any(waiting.values()) and self.clock() - finished_at >= diag_grace):
125
+ return t
126
+ if self.clock() - start >= timeout:
127
+ return t
128
+ self.sleep(poll)
129
+
130
+
131
+ def q(s):
132
+ return urllib.parse.quote(str(s), safe="")
133
+
134
+
135
+ def runs_done(t):
136
+ p = t.get("progress") or {}
137
+ return bool(p.get("total")) and p.get("done") == p.get("total") and (t.get("gate") or {}).get("verdict") != "pending"
138
+
139
+
140
+ def diagnoses_pending(t, item_ids=None):
141
+ """{item_id: started?} for the cases whose diagnosis is still to come."""
142
+ want = set(item_ids) if item_ids else None
143
+ out = {}
144
+ for r in t.get("rows") or []:
145
+ if want is not None:
146
+ if r["item_id"] not in want:
147
+ continue
148
+ elif r.get("kind") != "regressed":
149
+ continue
150
+ rca = r.get("rca") or {}
151
+ if rca.get("status") not in ("done", "error"):
152
+ out[r["item_id"]] = bool(rca)
153
+ return out
154
+
155
+
156
+ def _clip(s, n=1500):
157
+ s = s or ""
158
+ return s if len(s) <= n else s[: n - 1] + "…"
159
+
160
+
161
+ def diagnosis(rca):
162
+ if not rca:
163
+ return None
164
+ if rca.get("status") != "done":
165
+ return {"status": rca.get("status") or "queued", **({"error": _clip(rca.get("error"), 300)} if rca.get("error") else {})}
166
+ res = rca.get("result") or {}
167
+ d, fix, ver = res.get("diagnosis") or {}, res.get("fix") or {}, res.get("verification") or None
168
+ out = {"status": "done", "cause": d.get("cause"), "kind": d.get("kind"), "explanation": _clip(d.get("explanation")),
169
+ "agreement": res.get("agreement"), "needs_review": bool(res.get("needs_review")),
170
+ "fix": {k: fix[k] for k in ("type", "summary", "prompt_edits", "test_fix", "risky") if fix.get(k)}}
171
+ if ver:
172
+ out["fix_verified"] = {"status": ver.get("status"), "note": _clip(ver.get("note"), 400)}
173
+ prior = res.get("prior") or {}
174
+ if prior.get("flagged"):
175
+ out["prior_disagrees"] = {"cause": prior.get("cause"), "note": "a confirmed past diagnosis of a similar failure says otherwise; check before acting"}
176
+ return out
177
+
178
+
179
+ def _side(s):
180
+ return {"label": s.get("label"), "score": s.get("score")} if s else None
181
+
182
+
183
+ def summary(t, url):
184
+ """What a coding agent needs from a change test: the verdict, why, and what to do next."""
185
+ gate, test = t.get("gate") or {}, t.get("test") or {}
186
+ rows = t.get("rows") or []
187
+ done = runs_done(t)
188
+ bs, cs = t.get("baseline_summary") or {}, t.get("candidate_summary") or {}
189
+ num = lambda s: {k: s.get(k) for k in ("passed", "cases", "avg_score") if k in s}
190
+ failing = [r for r in rows if r.get("kind") in FAILING]
191
+ still_failing = [r for r in rows if r.get("kind") in ("same", "worse") and (r.get("candidate") or {}).get("label") not in (None, PASS)]
192
+ out = {
193
+ "test_id": test.get("test_id"), "project_id": test.get("project_id"), "url": url,
194
+ "status": "done" if done else "running",
195
+ "progress": f"{(t.get('progress') or {}).get('done', 0)}/{(t.get('progress') or {}).get('total', 0)} answers",
196
+ "verdict": gate.get("verdict"),
197
+ **({"would_be": gate["would_be"]} if gate.get("would_be") else {}),
198
+ **({"stage": gate["stage"]} if gate.get("stage") else {}),
199
+ "rules": [{"rule": r.get("rule"), "passed": r.get("passed"), "detail": r.get("detail")} for r in gate.get("rules") or []],
200
+ "counts": t.get("counts"),
201
+ "baseline": {"label": (t.get("baseline") or {}).get("label"), **num(bs)},
202
+ "candidate": {"label": (t.get("candidate") or {}).get("label"), "note": (t.get("candidate") or {}).get("note"), **num(cs)},
203
+ "regressions": [{"item_id": r["item_id"], "title": r.get("title"), "kind": r["kind"], "tags": r.get("tags") or [],
204
+ "before": _side(r.get("baseline")), "after": _side(r.get("candidate")),
205
+ "diagnosis": diagnosis(r.get("rca"))} for r in failing],
206
+ "still_failing": [{"item_id": r["item_id"], "title": r.get("title"), "kind": r["kind"], "after": _side(r.get("candidate")),
207
+ **({"new_failures": r["new_failures"]} if r.get("new_failures") else {}),
208
+ **({"diagnosis": diagnosis(r["rca"])} if r.get("rca") else {})} for r in still_failing],
209
+ }
210
+ out["next"] = next_step(out)
211
+ return out
212
+
213
+
214
+ def next_step(s):
215
+ if s["status"] != "done":
216
+ return f"Still running ({s['progress']}). Call get_change_test with test_id {s['test_id']} and wait=true."
217
+ v = s["verdict"]
218
+ if v == "ready":
219
+ return ("The gate passes. Shipping (promoting to production) is a person's decision: share the url, or let CI "
220
+ "run the same test on the pull request.")
221
+ diags = [r["diagnosis"] for r in s["regressions"] if r.get("diagnosis")]
222
+ if any(d.get("status") != "done" for d in diags) or len(diags) < len(s["regressions"]):
223
+ return "Diagnoses are still running; call get_change_test with wait=true to read the causes and fixes."
224
+ if any(d.get("cause") == "TEST" for d in diags):
225
+ hint = " Where the cause is TEST, the test itself is wrong: tell the user rather than changing the product to fit it."
226
+ else:
227
+ hint = ""
228
+ if any((d.get("fix") or {}).get("prompt_edits") for d in diags):
229
+ return ("Blocked. Apply each regression's prompt_edits (find → replace) to the system prompt, or write a better fix, "
230
+ "then call test_change again. Don't remove or weaken test cases to pass the gate." + hint)
231
+ if v in ("blocked", "partial"):
232
+ return ("Blocked. Read each regression's diagnosis and fix, change the prompt, model, tools or code, and call "
233
+ "test_change again. Don't remove or weaken test cases to pass the gate." + hint)
234
+ return f"Verdict: {v}."
@@ -0,0 +1,116 @@
1
+ """NinjaEval MCP server: a coding agent tests an AI change (prompt, model, tools, code) before calling it done.
2
+
3
+ Runs over stdio; it opens no network port. It acts as one workspace, through that workspace's API key.
4
+ It can start change tests and diagnoses but can't ship: promoting a version to production stays with a person.
5
+ """
6
+ import os
7
+ from typing import List, Optional
8
+
9
+ try: # mcp 2.x
10
+ from mcp.server.mcpserver import MCPServer as _Server
11
+ except ImportError: # mcp 1.x
12
+ from mcp.server.fastmcp import FastMCP as _Server
13
+
14
+ from .api import Client, NinjaEvalError, summary
15
+
16
+ mcp = _Server("ninjaeval", instructions=(
17
+ "NinjaEval is a release gate for AI changes. After you change an AI app's system prompt, model, tools or agent code, "
18
+ "call test_change: it replays the project's test cases on production and on your change, grades every answer, and "
19
+ "returns a ready / blocked verdict with the cause and a fix for each regression. Don't call a prompt or model change "
20
+ "done while the verdict is blocked. Each test runs the real app and an AI judge, so it costs money and takes minutes: "
21
+ "test once per meaningful change, not after every keystroke. Never delete or weaken test cases to pass the gate."))
22
+
23
+ _client = None
24
+ _started = 0
25
+ MAX_TESTS = int(os.environ.get("NINJAEVAL_MCP_MAX_TESTS", "10")) # per server session, against an agent looping on fixes
26
+
27
+
28
+ def client():
29
+ global _client
30
+ if _client is None:
31
+ _client = Client()
32
+ return _client
33
+
34
+
35
+ @mcp.tool()
36
+ def whoami() -> dict:
37
+ """The workspace this server acts in, and what its API key may do."""
38
+ return client().me()
39
+
40
+
41
+ @mcp.tool()
42
+ def list_projects() -> list:
43
+ """The workspace's projects (AI apps under test). Use project_id in the other tools."""
44
+ return client().projects()
45
+
46
+
47
+ @mcp.tool()
48
+ def get_project(project_id: str) -> dict:
49
+ """A project's production version (provider, model, system prompt, tools), release gate, test suites and recent
50
+ change tests. Read this before test_change to see what production is."""
51
+ return client().project(project_id)
52
+
53
+
54
+ @mcp.tool()
55
+ def test_change(project_id: str, system_prompt: Optional[str] = None, model: Optional[str] = None,
56
+ provider: Optional[str] = None, params: Optional[dict] = None, tools: Optional[List[dict]] = None,
57
+ note: str = "", code_sha: str = "", stage: Optional[str] = None, trials: int = 1,
58
+ wait: bool = True, timeout_seconds: int = 600) -> dict:
59
+ """Test a change against production. Copies production and applies only the fields you pass: system_prompt (the
60
+ full new prompt), model, provider, params, tools ([{name, description, parameters}]). For an endpoint project whose
61
+ change is in code, deploy it where the project's endpoint runs it and pass code_sha. note says what changed and why.
62
+ stage runs only that stage's cases (e.g. "smoke"; quick, can't ship). trials > 1 runs each case several times.
63
+
64
+ With wait=true (default) it returns when the verdict and the regressions' diagnoses are in, or at timeout_seconds
65
+ (then call get_change_test). Returns verdict (ready / blocked / partial / pending), the gate's rules, each regression
66
+ with its diagnosis (cause PROMPT / MODEL / TOOL / TEST, explanation, fix with prompt_edits), a url and a next step."""
67
+ global _started
68
+ if _started >= MAX_TESTS:
69
+ raise NinjaEvalError(f"This session already started {MAX_TESTS} change tests (NINJAEVAL_MCP_MAX_TESTS). "
70
+ "Stop and show the user the results so far.")
71
+ c = client()
72
+ t = c.start_change_test(project_id, system_prompt=system_prompt, model=model, provider=provider, params=params,
73
+ tools=tools, note=note or "Change from a coding agent (MCP)", code_sha=code_sha, stage=stage,
74
+ trials=max(1, min(int(trials), 5)))
75
+ _started += 1
76
+ test_id = t["test_id"]
77
+ full = c.wait(test_id, timeout=_timeout(timeout_seconds)) if wait else c.test(test_id)
78
+ return summary(full, c.test_url(test_id))
79
+
80
+
81
+ @mcp.tool()
82
+ def get_change_test(test_id: str, wait: bool = False, timeout_seconds: int = 600) -> dict:
83
+ """A change test's verdict, rules, regressions with their diagnoses and fixes, and the next step. With wait=true,
84
+ first waits for the runs and the diagnoses to finish (up to timeout_seconds)."""
85
+ c = client()
86
+ full = c.wait(test_id, timeout=_timeout(timeout_seconds)) if wait else c.test(test_id)
87
+ return summary(full, c.test_url(test_id))
88
+
89
+
90
+ @mcp.tool()
91
+ def diagnose_cases(test_id: str, item_ids: List[str], wait: bool = True, timeout_seconds: int = 300) -> dict:
92
+ """Diagnose failing cases that weren't diagnosed automatically (regressions are). item_ids come from
93
+ get_change_test's still_failing. Each diagnosis runs 3 times and re-runs its fix to check it."""
94
+ c = client()
95
+ c.analyze(test_id, item_ids)
96
+ full = c.wait(test_id, timeout=_timeout(timeout_seconds), item_ids=item_ids) if wait else c.test(test_id)
97
+ return summary(full, c.test_url(test_id))
98
+
99
+
100
+ @mcp.tool()
101
+ def get_report(test_id: str) -> str:
102
+ """The change test's full report as Markdown: both versions, every case result, the gate and the diagnoses.
103
+ Good for a pull request description."""
104
+ return client().report(test_id)
105
+
106
+
107
+ def _timeout(s):
108
+ return max(10, min(int(s), 1800))
109
+
110
+
111
+ def main():
112
+ mcp.run("stdio") # stdio only: no network listener
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: ninjaeval-mcp
3
+ Version: 0.1.0
4
+ Summary: NinjaEval as an MCP server: a coding agent tests a prompt, model or tool change and reads the verdict and the diagnosis before calling it done.
5
+ License: MIT
6
+ Project-URL: Homepage, https://ninjaeval.com
7
+ Project-URL: Documentation, https://ninjaeval.com/docs/developer-guide.html
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: mcp<3,>=1.2
12
+ Dynamic: license-file
13
+
14
+ # NinjaEval MCP server
15
+
16
+ Lets your coding agent (Claude Code, Cursor, Copilot, any MCP client) test an AI change with
17
+ [NinjaEval](https://ninjaeval.com) before it calls the change done.
18
+
19
+ The agent edits your bot's system prompt, model or tools, then calls `test_change`. NinjaEval replays your test cases
20
+ on production and on the change and grades every answer. It returns **ready** or **blocked**, with the cause of each
21
+ regression and a fix that was re-run to check it. The agent applies the fix and tests again. Shipping stays with you:
22
+ the server can't promote a version to production.
23
+
24
+ ## Set up
25
+
26
+ 1. In NinjaEval, open **⚙ Workspace & API keys** and create a key for the agent (e.g. `coding-agent`).
27
+ 2. Add the server to your MCP client. For Claude Code:
28
+
29
+ ```bash
30
+ claude mcp add ninjaeval --env NINJAEVAL_API_KEY=efk_... -- uvx ninjaeval-mcp
31
+ ```
32
+
33
+ Any other client (`mcpServers` in its JSON config):
34
+
35
+ ```json
36
+ { "mcpServers": { "ninjaeval": { "command": "uvx", "args": ["ninjaeval-mcp"],
37
+ "env": { "NINJAEVAL_API_KEY": "efk_..." } } } }
38
+ ```
39
+
40
+ Or `pip install ninjaeval-mcp` and run `ninjaeval-mcp`.
41
+
42
+ It runs over stdio on your machine and opens no network port. It talks only to `api.ninjaeval.com`.
43
+
44
+ ## Tools
45
+
46
+ | Tool | What it does |
47
+ |---|---|
48
+ | `whoami` | The workspace, the key's role, today's limits |
49
+ | `list_projects` | Your projects (AI apps under test) |
50
+ | `get_project` | Production's provider, model, system prompt and tools; the gate; suites; recent change tests |
51
+ | `test_change` | Tests a change against production: only the fields you pass change (`system_prompt`, `model`, `provider`, `params`, `tools`, `code_sha`, `note`, `stage`, `trials`). Waits for the verdict and the diagnoses |
52
+ | `get_change_test` | A test's verdict, rules, regressions with cause / explanation / fix (`prompt_edits`) / whether the fix was verified, cases still failing, and the next step |
53
+ | `diagnose_cases` | Diagnoses failing cases that weren't diagnosed automatically |
54
+ | `get_report` | The full report as Markdown, e.g. for a PR description |
55
+
56
+ Each response has a `next` field telling the agent what to do. When the cause is `TEST`, the test itself is wrong, so
57
+ the agent should tell you rather than bend the product to fit it.
58
+
59
+ ## Settings
60
+
61
+ | Variable | Default | |
62
+ |---|---|---|
63
+ | `NINJAEVAL_API_KEY` | | Workspace key (`efk_…`) |
64
+ | `NINJAEVAL_API_KEY_FILE` | | Read the key from a file instead |
65
+ | `NINJAEVAL_API_URL` | `https://api.ninjaeval.com/api/v1/eng` | |
66
+ | `NINJAEVAL_APP_URL` | `https://ninjaeval.com` | Used for the links in responses |
67
+ | `NINJAEVAL_MCP_MAX_TESTS` | `10` | Change tests one session may start, so an agent can't loop on fixes indefinitely |
68
+
69
+ Each change test runs your real app and an AI judge, which takes minutes and counts toward the workspace's daily
70
+ limits. Endpoint projects call your service with the system prompt NinjaEval sends. If your service ignores it, test
71
+ code changes by deploying them where the endpoint runs and passing `code_sha`.
@@ -0,0 +1,9 @@
1
+ ninjaeval_mcp/__init__.py,sha256=v0WybslcsUSZFOdphi8QbZP7KzjcMWHCUxUqXuc5qiU,112
2
+ ninjaeval_mcp/api.py,sha256=Ijo4ZwNYb5WPelLd4_7zWzQKodn-tCvVumN8EnWdQ1Q,11505
3
+ ninjaeval_mcp/server.py,sha256=Y2L3L5q2Xsg6wHq8ESxfFLm7tywh5wgYEeJaL7RWmWw,5562
4
+ ninjaeval_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=fHJAF0hijeS_sAkdlfykh85X_0241uZohegGFxSbCW0,1066
5
+ ninjaeval_mcp-0.1.0.dist-info/METADATA,sha256=-tlbKxzpllalcjICCYmg4jxPoYoYraH07d5zcd5JeTw,3520
6
+ ninjaeval_mcp-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ ninjaeval_mcp-0.1.0.dist-info/entry_points.txt,sha256=aIwwm6onhWLrF1GwX_vNNSWMR3IzztaZrJf6TrIehgw,60
8
+ ninjaeval_mcp-0.1.0.dist-info/top_level.txt,sha256=d3MfGEisXJnv4BA7P1nSfxDYaAvPdXgsobhzqkAdTbQ,14
9
+ ninjaeval_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ninjaeval-mcp = ninjaeval_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NinjaEval
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.
@@ -0,0 +1 @@
1
+ ninjaeval_mcp