technical-answer-validator 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- technical_answer_validator-0.1.2/PKG-INFO +113 -0
- technical_answer_validator-0.1.2/PUBLISHING.md +53 -0
- technical_answer_validator-0.1.2/README.md +103 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/mcp_server.py +49 -42
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/pyproject.toml +36 -38
- technical_answer_validator-0.1.2/scripts/query_telemetry.py +46 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/server.json +20 -20
- technical_answer_validator-0.1.2/tav_analytics.py +138 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/uv.lock +1 -1
- technical_answer_validator-0.1.1/PKG-INFO +0 -35
- technical_answer_validator-0.1.1/PUBLISHING.md +0 -31
- technical_answer_validator-0.1.1/README.md +0 -25
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.dockerignore +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.env.example +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.github/workflows/ci.yml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.github/workflows/publish-container.yml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.github/workflows/publish-mcp-registry.yml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.github/workflows/publish-pypi.yml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.gitignore +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/Dockerfile +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/LICENSE +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/NOTICE.md +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/claude-mcp-config.example.json +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/codex-mcp-config.example.toml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/compose.yaml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/openapi.yaml +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/requirements.txt +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/scripts/create_api_key.py +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tav_api.py +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tav_core.py +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tests/test_api.py +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tests/test_mcp_server.py +0 -0
- {technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tests/test_tav.py +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: technical-answer-validator
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: MCP and REST tools for deterministic rubric-based technical answer review
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: mcp==2.2.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Technical Answer Validator
|
|
12
|
+
|
|
13
|
+
<!-- mcp-name: io.github.Christofer566/technical-answer-validator -->
|
|
14
|
+
|
|
15
|
+
A tiny, deterministic answer review tool for AI agents, available over **MCP stdio** and as a REST API. Each caller supplies the concepts, accepted synonyms, numeric requirements, and answer text for a single evaluation. It does not include a question bank or answer corpus.
|
|
16
|
+
|
|
17
|
+
This is an **assistive practice tool**, not an official certification exam grader. Keyword matching can miss semantically correct paraphrases and can accept misleading surface matches. Users should review the supplied rubric and every result.
|
|
18
|
+
|
|
19
|
+
## Run locally
|
|
20
|
+
|
|
21
|
+
Python 3.10+; the REST server uses the standard library. The MCP adapter uses the official Python SDK 2.2.0.
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
$env:TAV_API_KEY = "replace-with-a-long-random-secret-at-least-24-characters"
|
|
25
|
+
python -m tav_api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The service listens on `127.0.0.1:8080` by default. To change it, set `TAV_HOST` and `TAV_PORT`. Local single-key mode refuses to start without a `TAV_API_KEY` of at least 24 characters. For deployment, create a distinct key per caller with `python scripts/create_api_key.py CLIENT_ID` and configure `TAV_API_KEYS` as a JSON object mapping each client ID to the generated SHA-256 digest. Store the one-time raw key with that client; do not store or commit it in this repository. When `TAV_API_KEYS` is set, it takes precedence over `TAV_API_KEY`.
|
|
29
|
+
|
|
30
|
+
## MCP for AI agents
|
|
31
|
+
|
|
32
|
+
Install the pinned MCP SDK 2.2.0 in a virtual environment from the committed lockfile:
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
uv sync --locked
|
|
36
|
+
.\.venv\Scripts\Activate.ps1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The stdio MCP server exposes one tool: `evaluate_answer(rubric, answer)`. Configure an MCP host with the absolute path to the environment's Python and `mcp_server.py`. Example Claude Desktop configuration (replace paths):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"technical-answer-validator": {
|
|
45
|
+
"command": "C:\\path\\to\\technical-answer-validator\\.venv\\Scripts\\python.exe",
|
|
46
|
+
"args": ["C:\\path\\to\\technical-answer-validator\\mcp_server.py"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After publishing to PyPI, run with `uvx --from technical-answer-validator tav-mcp`. For local development use the `.venv` Python plus `mcp_server.py`. For Codex CLI or another MCP host, use its stdio server configuration with that command and script path. Restart the host, then ask it to list tools and call `evaluate_answer`. The stdio transport is local to the user's agent host and needs no internet endpoint or API key.
|
|
53
|
+
|
|
54
|
+
The Codex TOML template is `codex-mcp-config.example.toml`; the Claude Desktop JSON template is `claude-mcp-config.example.json`. Replace both placeholder paths with absolute paths. Merge the block into the host configuration; do not overwrite other MCP servers or global settings.
|
|
55
|
+
|
|
56
|
+
### Local MCP usage counts
|
|
57
|
+
|
|
58
|
+
The MCP server keeps local daily aggregate counts for tool calls, successful evaluations, invalid requests, and internal errors. It never stores rubric or answer text and sends nothing over the network. Counts are retained for 90 days in `~/.technical-answer-validator/usage.sqlite3` (Windows: the user's home directory). Run `tav-mcp-stats` to print a JSON report. Set `TAV_ANALYTICS=off` in the MCP server environment to disable counting; set `TAV_ANALYTICS_DB` to choose another local database path. These counts remain on the user's computer unless the user voluntarily shares the report.
|
|
59
|
+
|
|
60
|
+
To **voluntarily share anonymous usage counts** with the maintainers, enable remote telemetry in the MCP server environment only after reviewing this notice:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
TAV_TELEMETRY=on
|
|
64
|
+
TAV_TELEMETRY_URL=https://technical-answer-validator-telemetry.chl1591204.workers.dev/v1/event
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Remote telemetry is off by default. When enabled, each tool call sends only a random persistent installation ID, package version, and outcome (`success`, `invalid_request`, or `error`) to the configured Cloudflare Worker. The ID is random and is not derived from a user, device, or account identifier; it lets us estimate distinct installations, not distinct people. Answers, rubrics, tool outputs, IP addresses, host names, and request contents are not included in the event payload. Cloudflare necessarily receives network connection metadata such as the sender IP to deliver the request; our Worker does not log or store it. Analytics Engine retains event data for three months. Disable by setting `TAV_TELEMETRY=off` and optionally remove `~/.technical-answer-validator/telemetry-id` to reset the random ID. Telemetry failures never interrupt evaluations.
|
|
68
|
+
|
|
69
|
+
The collector is deployed at the URL above and accepts events only at `/v1/event`. To read aggregate usage, create a Cloudflare API token restricted to **Account Analytics Read** for the account that owns the Worker, then run `python scripts/query_telemetry.py` from this repository. The script prompts for the token without echoing or saving it. The report groups call totals by UTC day, package version, and outcome, and estimates distinct installations; it does not identify people. Do not put the token in source control, an MCP configuration, or a command line.
|
|
70
|
+
|
|
71
|
+
PyPI 0.1.1 does not include this opt-in telemetry code. It will be available to package users in 0.1.2; collection starts only after that version is installed and a user explicitly enables both environment variables above. The local-only `tav-mcp-stats` command continues to work independently.
|
|
72
|
+
|
|
73
|
+
## Request
|
|
74
|
+
|
|
75
|
+
`POST /v1/evaluate`
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"rubric": {
|
|
80
|
+
"required_concepts": ["isolation", "lockout tag"],
|
|
81
|
+
"accepted_synonyms": {"isolation": ["energy isolation"]},
|
|
82
|
+
"numeric_requirements": [],
|
|
83
|
+
"required_count": 2
|
|
84
|
+
},
|
|
85
|
+
"answer": "Apply energy isolation and attach a lockout tag."
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`accepted_synonyms` keys must exactly match a concept. `numeric_requirements` is an optional array such as `[{"value":"10","unit":"kN","tolerance":"0"}]`. Numbers in an answer are only checked when explicit requirements are provided. Concept score is matched concepts / required_count (defaults to the number of concepts), capped at 1.0; numeric failures apply a 50% score penalty. The verdict thresholds are `correct >= 0.8`, `partial >= 0.4`, otherwise `wrong`.
|
|
90
|
+
|
|
91
|
+
## Response and errors
|
|
92
|
+
|
|
93
|
+
Successful requests return `api_version`, `status`, `score`, `verdict`, matched/missing concepts, numeric check details, and `review_required: true`.
|
|
94
|
+
|
|
95
|
+
Errors use JSON `{ "error": { "code": "...", "message": "..." } }`. Statuses include 400 (invalid request), 401 (missing/invalid key), 404, 405, 413 (body over 64 KiB), and 429 (over 60 requests/minute per client ID). The rate limit is 60 requests/minute per authenticated client ID, in memory, and resets when the process restarts. Daily request/success/client-error/rate-limited counters are persisted in SQLite without answer text and expire after 90 days. `GET /v1/usage` returns only the caller's current UTC-day counts. Retain and back up the usage volume as desired; it contains client IDs and aggregates only.
|
|
96
|
+
|
|
97
|
+
## Privacy and deployment limits
|
|
98
|
+
|
|
99
|
+
The stdio MCP option runs locally and sends no usage information to a central service unless the user explicitly enables remote telemetry as described above. Local aggregate call counts can be disabled separately. The REST API has separate server-side aggregates per API client.
|
|
100
|
+
|
|
101
|
+
The server does not log request bodies or answers. It stores daily counts keyed by client ID and request timestamps in process memory for REST rate limiting. The **stdio MCP option runs locally inside the agent host** and sends no requests to this HTTP server. The REST API is containerized and keeps usage counters in a persistent volume. Before public service, terminate TLS at a reverse proxy, set proxy-level rate/concurrency limits, deploy from a secret manager, monitor the host, and publish a data-retention/contact policy. The app-level per-client rate limit resets on restart and is not a substitute for edge controls.
|
|
102
|
+
|
|
103
|
+
## Verify
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
python -m unittest discover -s tests -v
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The OpenAPI contract is in `openapi.yaml`; the draft official MCP Registry descriptor is `server.json`, with publication steps in `PUBLISHING.md`. Run locally with Docker Compose after copying `.env.example` to `.env` and adding a private key; Compose publishes the service only on loopback, so configure an HTTPS reverse proxy separately. `compose.yaml` persists aggregate usage in a named volume and applies a read-only root filesystem, dropped Linux capabilities, and resource limits.
|
|
110
|
+
|
|
111
|
+
These tests check API and grading behavior; they do not establish professional exam accuracy.
|
|
112
|
+
|
|
113
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Public release runbook
|
|
2
|
+
|
|
3
|
+
The package contains two delivery paths: local MCP stdio for an agent that can spawn a local process, and a REST API container for hosted agents. GitHub and PyPI releases are public, and the container image is published. The REST service itself still needs a host deployment and a public HTTPS endpoint.
|
|
4
|
+
|
|
5
|
+
The stdio server records local daily aggregate call counts. Version 0.1.2 adds optional remote MCP usage telemetry, disabled by default and requiring explicit user opt-in; the collector runs on Cloudflare Workers and the provider reads aggregate usage through the Analytics Engine SQL API. The REST host remains deferred.
|
|
6
|
+
|
|
7
|
+
## Local MCP package
|
|
8
|
+
|
|
9
|
+
1. MIT was selected and is included in `LICENSE` and the Python package metadata. Confirm that all included source/assets are eligible for MIT before public release.
|
|
10
|
+
2. The public GitHub repository exists at `https://github.com/Christofer566/technical-answer-validator`; source is on `main`. Keep `.venv`, `.uv-cache`, `.env`, usage databases, and generated state out of commits.
|
|
11
|
+
3. Build and inspect the wheel/sdist: `uv build`; test the wheel in a clean virtual environment; `uv run python -m unittest discover -s tests -v`.
|
|
12
|
+
4. PyPI 0.1.1 is already published. The pending GitHub Actions Trusted Publisher is configured for project `technical-answer-validator`, owner `Christofer566`, repository `technical-answer-validator`, workflow `.github/workflows/publish-pypi.yml`, environment `pypi`. For 0.1.2, merge the reviewed telemetry change, build and inspect the wheel/sdist, then publish through the configured trusted-publisher workflow using tag `v0.1.2`.
|
|
13
|
+
5. Confirm the built PyPI README contains the exact `mcp-name` marker and that package metadata, repository URL, license, and author are correct.
|
|
14
|
+
6. Validate `server.json` against the current MCP Registry schema and verify the GitHub/PyPI namespace, package name, and version. Then publish with the official MCP Registry CLI using the owner's GitHub identity.
|
|
15
|
+
7. Test installation from the published package in a clean environment with `uvx --from technical-answer-validator tav-mcp`; connect it in target agent hosts and call `evaluate_answer`.
|
|
16
|
+
|
|
17
|
+
## Hosted REST API
|
|
18
|
+
|
|
19
|
+
### Deployment decision
|
|
20
|
+
|
|
21
|
+
Do not create the Render service yet. The stdio MCP package is already public and runs on the user's own computer. The REST API source and GHCR image are also public for self-hosting. There is no confirmed demand for an always-on API operated by us, so the estimated recurring Render cost and service operation are not justified yet.
|
|
22
|
+
|
|
23
|
+
Review managed hosting 30 days after first external MCP announcement. Check installs/downloads, successful connections, support requests, and whether users specifically need an API URL operated by us. If there is no concrete hosted-API request, review once more at day 90. If there is still no evidence or willingness to pay for this convenience, defer hosting and review quarterly only when usage signals change.
|
|
24
|
+
|
|
25
|
+
Consider Render only when at least one real user concretely requests an operator-managed HTTPS endpoint because local MCP/self-hosting is unsuitable, and they confirm willingness to pay or participate in a bounded trial. Before creation, set a monthly spend cap, name an operator, define key issuance/revocation and data retention, and verify actual billing. Do not buy a custom domain until demand is validated.
|
|
26
|
+
|
|
27
|
+
### When the trigger is met
|
|
28
|
+
|
|
29
|
+
`render.yaml` is a prepared independent Render configuration (Singapore, Starter, HTTPS, `/healthz`, and a 1 GB persistent disk at `/data`). Expected starting cost is about $7.25/month before taxes/overages; verify current pricing in Render before creating the service. This deployment is independent of Windows and the existing VPS.
|
|
30
|
+
|
|
31
|
+
1. Set `TAV_API_KEYS` in the Render secret environment to `{client_id: sha256_hex}` entries generated by `python scripts/create_api_key.py CLIENT_ID`. Deliver each raw key privately to its intended agent operator; only the digest belongs in deployment configuration.
|
|
32
|
+
2. Published image: `ghcr.io/christofer566/technical-answer-validator-api:0.1.1` (also `:latest`), built by the successful GitHub Actions workflow for tag `v0.1.1`. Deploy the image to the selected Render service, retaining the `/data` disk. Add platform-level request and rate controls; app-level rate buckets reset on restart.
|
|
33
|
+
3. Monitor `/healthz`, per-customer `/v1/usage`, host availability, disk space for SQLite aggregates, and spend. Define key rotation/revocation by removing the corresponding digest and restarting/redeploying.
|
|
34
|
+
4. Verify external HTTPS from a separate network with unauthenticated rejection, valid client isolation, 64 KiB limit, rate limits, no answer logging, and accurate OpenAPI docs before announcing a URL.
|
|
35
|
+
|
|
36
|
+
## Current deployment progress and remaining prerequisites
|
|
37
|
+
|
|
38
|
+
- Public source repository: `https://github.com/Christofer566/technical-answer-validator`; `server.json` points to it.
|
|
39
|
+
- PyPI 0.1.1: `https://pypi.org/project/technical-answer-validator/0.1.1/`.
|
|
40
|
+
- GitHub Release v0.1.1: `https://github.com/Christofer566/technical-answer-validator/releases/tag/v0.1.1`.
|
|
41
|
+
- GHCR image publication and CI succeeded for v0.1.1.
|
|
42
|
+
- Render hosting plan and `render.yaml` are prepared, but service creation is intentionally deferred pending demand. Review 30 days after external MCP announcement, then at day 90 if the first review finds no concrete hosted-API need.
|
|
43
|
+
- This API deployment is intentionally independent of the VPS and Windows runtime.
|
|
44
|
+
- MIT is selected. Public source ownership and rights to included files should remain confirmed by the owner.
|
|
45
|
+
- No public API URL has been created; image publication is not a running service.
|
|
46
|
+
- The tool's keyword/numeric decisions are not validated against independent subject-matter gold data. Position it as assistive agent feedback, not a trustworthy final grader.
|
|
47
|
+
## Provider usage measurement (0.1.2 candidate)
|
|
48
|
+
|
|
49
|
+
The opt-in MCP telemetry collector is deployed at `https://technical-answer-validator-telemetry.chl1591204.workers.dev`. Cloudflare Analytics Engine dataset `tav_mcp_usage` is bound as `TAV_USAGE`; the Worker accepts `/v1/event` and does not log request bodies. Remote telemetry is disabled by default. MCP users must install the 0.1.2 release and explicitly set `TAV_TELEMETRY=on` plus `TAV_TELEMETRY_URL` to opt in. The event contains only a random installation UUID, package version, and outcome. No answer, rubric, tool output, host name, or account identifier is sent.
|
|
50
|
+
|
|
51
|
+
The provider query utility is `python scripts/query_telemetry.py`. It asks for a Cloudflare Account Analytics Read API token using hidden input and does not persist it. The query estimates unique installations, not unique people, and reports UTC daily totals by package version and outcome. Analytics Engine retains data for three months. Do not publish the provider token or put it in MCP configuration.
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Technical Answer Validator
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.Christofer566/technical-answer-validator -->
|
|
4
|
+
|
|
5
|
+
A tiny, deterministic answer review tool for AI agents, available over **MCP stdio** and as a REST API. Each caller supplies the concepts, accepted synonyms, numeric requirements, and answer text for a single evaluation. It does not include a question bank or answer corpus.
|
|
6
|
+
|
|
7
|
+
This is an **assistive practice tool**, not an official certification exam grader. Keyword matching can miss semantically correct paraphrases and can accept misleading surface matches. Users should review the supplied rubric and every result.
|
|
8
|
+
|
|
9
|
+
## Run locally
|
|
10
|
+
|
|
11
|
+
Python 3.10+; the REST server uses the standard library. The MCP adapter uses the official Python SDK 2.2.0.
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
$env:TAV_API_KEY = "replace-with-a-long-random-secret-at-least-24-characters"
|
|
15
|
+
python -m tav_api
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The service listens on `127.0.0.1:8080` by default. To change it, set `TAV_HOST` and `TAV_PORT`. Local single-key mode refuses to start without a `TAV_API_KEY` of at least 24 characters. For deployment, create a distinct key per caller with `python scripts/create_api_key.py CLIENT_ID` and configure `TAV_API_KEYS` as a JSON object mapping each client ID to the generated SHA-256 digest. Store the one-time raw key with that client; do not store or commit it in this repository. When `TAV_API_KEYS` is set, it takes precedence over `TAV_API_KEY`.
|
|
19
|
+
|
|
20
|
+
## MCP for AI agents
|
|
21
|
+
|
|
22
|
+
Install the pinned MCP SDK 2.2.0 in a virtual environment from the committed lockfile:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
uv sync --locked
|
|
26
|
+
.\.venv\Scripts\Activate.ps1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The stdio MCP server exposes one tool: `evaluate_answer(rubric, answer)`. Configure an MCP host with the absolute path to the environment's Python and `mcp_server.py`. Example Claude Desktop configuration (replace paths):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"technical-answer-validator": {
|
|
35
|
+
"command": "C:\\path\\to\\technical-answer-validator\\.venv\\Scripts\\python.exe",
|
|
36
|
+
"args": ["C:\\path\\to\\technical-answer-validator\\mcp_server.py"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After publishing to PyPI, run with `uvx --from technical-answer-validator tav-mcp`. For local development use the `.venv` Python plus `mcp_server.py`. For Codex CLI or another MCP host, use its stdio server configuration with that command and script path. Restart the host, then ask it to list tools and call `evaluate_answer`. The stdio transport is local to the user's agent host and needs no internet endpoint or API key.
|
|
43
|
+
|
|
44
|
+
The Codex TOML template is `codex-mcp-config.example.toml`; the Claude Desktop JSON template is `claude-mcp-config.example.json`. Replace both placeholder paths with absolute paths. Merge the block into the host configuration; do not overwrite other MCP servers or global settings.
|
|
45
|
+
|
|
46
|
+
### Local MCP usage counts
|
|
47
|
+
|
|
48
|
+
The MCP server keeps local daily aggregate counts for tool calls, successful evaluations, invalid requests, and internal errors. It never stores rubric or answer text and sends nothing over the network. Counts are retained for 90 days in `~/.technical-answer-validator/usage.sqlite3` (Windows: the user's home directory). Run `tav-mcp-stats` to print a JSON report. Set `TAV_ANALYTICS=off` in the MCP server environment to disable counting; set `TAV_ANALYTICS_DB` to choose another local database path. These counts remain on the user's computer unless the user voluntarily shares the report.
|
|
49
|
+
|
|
50
|
+
To **voluntarily share anonymous usage counts** with the maintainers, enable remote telemetry in the MCP server environment only after reviewing this notice:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
TAV_TELEMETRY=on
|
|
54
|
+
TAV_TELEMETRY_URL=https://technical-answer-validator-telemetry.chl1591204.workers.dev/v1/event
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Remote telemetry is off by default. When enabled, each tool call sends only a random persistent installation ID, package version, and outcome (`success`, `invalid_request`, or `error`) to the configured Cloudflare Worker. The ID is random and is not derived from a user, device, or account identifier; it lets us estimate distinct installations, not distinct people. Answers, rubrics, tool outputs, IP addresses, host names, and request contents are not included in the event payload. Cloudflare necessarily receives network connection metadata such as the sender IP to deliver the request; our Worker does not log or store it. Analytics Engine retains event data for three months. Disable by setting `TAV_TELEMETRY=off` and optionally remove `~/.technical-answer-validator/telemetry-id` to reset the random ID. Telemetry failures never interrupt evaluations.
|
|
58
|
+
|
|
59
|
+
The collector is deployed at the URL above and accepts events only at `/v1/event`. To read aggregate usage, create a Cloudflare API token restricted to **Account Analytics Read** for the account that owns the Worker, then run `python scripts/query_telemetry.py` from this repository. The script prompts for the token without echoing or saving it. The report groups call totals by UTC day, package version, and outcome, and estimates distinct installations; it does not identify people. Do not put the token in source control, an MCP configuration, or a command line.
|
|
60
|
+
|
|
61
|
+
PyPI 0.1.1 does not include this opt-in telemetry code. It will be available to package users in 0.1.2; collection starts only after that version is installed and a user explicitly enables both environment variables above. The local-only `tav-mcp-stats` command continues to work independently.
|
|
62
|
+
|
|
63
|
+
## Request
|
|
64
|
+
|
|
65
|
+
`POST /v1/evaluate`
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"rubric": {
|
|
70
|
+
"required_concepts": ["isolation", "lockout tag"],
|
|
71
|
+
"accepted_synonyms": {"isolation": ["energy isolation"]},
|
|
72
|
+
"numeric_requirements": [],
|
|
73
|
+
"required_count": 2
|
|
74
|
+
},
|
|
75
|
+
"answer": "Apply energy isolation and attach a lockout tag."
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`accepted_synonyms` keys must exactly match a concept. `numeric_requirements` is an optional array such as `[{"value":"10","unit":"kN","tolerance":"0"}]`. Numbers in an answer are only checked when explicit requirements are provided. Concept score is matched concepts / required_count (defaults to the number of concepts), capped at 1.0; numeric failures apply a 50% score penalty. The verdict thresholds are `correct >= 0.8`, `partial >= 0.4`, otherwise `wrong`.
|
|
80
|
+
|
|
81
|
+
## Response and errors
|
|
82
|
+
|
|
83
|
+
Successful requests return `api_version`, `status`, `score`, `verdict`, matched/missing concepts, numeric check details, and `review_required: true`.
|
|
84
|
+
|
|
85
|
+
Errors use JSON `{ "error": { "code": "...", "message": "..." } }`. Statuses include 400 (invalid request), 401 (missing/invalid key), 404, 405, 413 (body over 64 KiB), and 429 (over 60 requests/minute per client ID). The rate limit is 60 requests/minute per authenticated client ID, in memory, and resets when the process restarts. Daily request/success/client-error/rate-limited counters are persisted in SQLite without answer text and expire after 90 days. `GET /v1/usage` returns only the caller's current UTC-day counts. Retain and back up the usage volume as desired; it contains client IDs and aggregates only.
|
|
86
|
+
|
|
87
|
+
## Privacy and deployment limits
|
|
88
|
+
|
|
89
|
+
The stdio MCP option runs locally and sends no usage information to a central service unless the user explicitly enables remote telemetry as described above. Local aggregate call counts can be disabled separately. The REST API has separate server-side aggregates per API client.
|
|
90
|
+
|
|
91
|
+
The server does not log request bodies or answers. It stores daily counts keyed by client ID and request timestamps in process memory for REST rate limiting. The **stdio MCP option runs locally inside the agent host** and sends no requests to this HTTP server. The REST API is containerized and keeps usage counters in a persistent volume. Before public service, terminate TLS at a reverse proxy, set proxy-level rate/concurrency limits, deploy from a secret manager, monitor the host, and publish a data-retention/contact policy. The app-level per-client rate limit resets on restart and is not a substitute for edge controls.
|
|
92
|
+
|
|
93
|
+
## Verify
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
python -m unittest discover -s tests -v
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The OpenAPI contract is in `openapi.yaml`; the draft official MCP Registry descriptor is `server.json`, with publication steps in `PUBLISHING.md`. Run locally with Docker Compose after copying `.env.example` to `.env` and adding a private key; Compose publishes the service only on loopback, so configure an HTTPS reverse proxy separately. `compose.yaml` persists aggregate usage in a named volume and applies a read-only root filesystem, dropped Linux capabilities, and resource limits.
|
|
100
|
+
|
|
101
|
+
These tests check API and grading behavior; they do not establish professional exam accuracy.
|
|
102
|
+
|
|
103
|
+
|
|
@@ -1,46 +1,53 @@
|
|
|
1
|
-
"""MCP stdio server exposing the answer evaluator as an agent tool."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from mcp.server import MCPServer
|
|
10
|
-
|
|
1
|
+
"""MCP stdio server exposing the answer evaluator as an agent tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mcp.server import MCPServer
|
|
10
|
+
|
|
11
11
|
from tav_core import RequestError, evaluate
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
from tav_analytics import record_call
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(message)s")
|
|
15
|
+
mcp = MCPServer(
|
|
16
|
+
"Technical Answer Validator",
|
|
17
|
+
instructions=(
|
|
18
|
+
"Evaluate a user's technical answer against a rubric supplied for this call. "
|
|
19
|
+
"Always pass the caller's rubric and answer. Explain that results are keyword-based "
|
|
20
|
+
"assistive feedback, never official exam grades; preserve review_required in your response."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
def evaluate_answer(rubric: dict[str, Any], answer: str) -> dict[str, Any]:
|
|
27
|
+
"""Check an answer against caller-provided concepts, synonyms, and numeric requirements.
|
|
28
|
+
|
|
29
|
+
Rubric fields: required_concepts (1-50 strings), optional accepted_synonyms keyed by
|
|
30
|
+
concept, optional numeric_requirements [{value, unit?, tolerance?}], and optional
|
|
31
|
+
required_count. No question bank is bundled. Review the returned result; it is not
|
|
32
|
+
an official exam grade.
|
|
33
|
+
"""
|
|
33
34
|
try:
|
|
34
|
-
|
|
35
|
+
result = evaluate({"rubric": rubric, "answer": answer})
|
|
36
|
+
record_call("success")
|
|
37
|
+
return result
|
|
35
38
|
except RequestError as exc:
|
|
36
|
-
|
|
39
|
+
record_call("invalid_request")
|
|
40
|
+
# Return a structured error result so the agent can repair its inputs.
|
|
37
41
|
return {"status": "invalid_request", "error": str(exc), "review_required": True}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
except Exception:
|
|
43
|
+
record_call("error")
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> None:
|
|
48
|
+
# FastMCP stdio transport uses stdout for protocol messages; keep diagnostics on stderr.
|
|
49
|
+
mcp.run(transport="stdio")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -1,42 +1,40 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.27,<2"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
license = "
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
[project.scripts]
|
|
18
|
-
tav-mcp = "mcp_server:main"
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "technical-answer-validator"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "MCP and REST tools for deterministic rubric-based technical answer review"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = ["mcp==2.2.0"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
tav-mcp = "mcp_server:main"
|
|
19
17
|
tav-api = "tav_api:main"
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
tav-mcp-stats = "tav_analytics:main"
|
|
19
|
+
|
|
22
20
|
[tool.hatch.build.targets.wheel]
|
|
23
|
-
only-include = ["mcp_server.py", "tav_core.py", "tav_api.py"]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"tav_core.py" = "tav_core.py"
|
|
21
|
+
only-include = ["mcp_server.py", "tav_core.py", "tav_api.py", "tav_analytics.py"]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
24
|
+
"mcp_server.py" = "mcp_server.py"
|
|
25
|
+
"tav_core.py" = "tav_core.py"
|
|
29
26
|
"tav_api.py" = "tav_api.py"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
[tool.hatch.build.targets.sdist]
|
|
33
|
-
only-include = [
|
|
34
|
-
".dockerignore", ".env.example", ".gitignore", "Dockerfile", "NOTICE.md",
|
|
35
|
-
"PUBLISHING.md", "README.md", "claude-mcp-config.example.json",
|
|
36
|
-
"codex-mcp-config.example.toml", "compose.yaml", "mcp_server.py",
|
|
37
|
-
"openapi.yaml", "pyproject.toml", "requirements.txt", "server.json",
|
|
38
|
-
"tav_api.py", "tav_core.py", "uv.lock", ".github", "scripts", "tests"
|
|
27
|
+
"tav_analytics.py" = "tav_analytics.py"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
only-include = [
|
|
31
|
+
".dockerignore", ".env.example", ".gitignore", "Dockerfile", "NOTICE.md",
|
|
32
|
+
"PUBLISHING.md", "README.md", "claude-mcp-config.example.json",
|
|
33
|
+
"codex-mcp-config.example.toml", "compose.yaml", "mcp_server.py",
|
|
34
|
+
"openapi.yaml", "pyproject.toml", "requirements.txt", "server.json",
|
|
35
|
+
"tav_api.py", "tav_core.py", "tav_analytics.py", "uv.lock", ".github", "scripts", "tests"
|
|
39
36
|
]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
[
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Query Cloudflare Analytics Engine without persisting the API token."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
QUERY = """
|
|
13
|
+
SELECT
|
|
14
|
+
toStartOfDay(timestamp) AS day,
|
|
15
|
+
blob1 AS version,
|
|
16
|
+
blob2 AS outcome,
|
|
17
|
+
SUM(double1 * _sample_interval) AS calls,
|
|
18
|
+
COUNT(DISTINCT index1) AS active_installations
|
|
19
|
+
FROM tav_mcp_usage
|
|
20
|
+
WHERE timestamp > NOW() - INTERVAL '90' DAY
|
|
21
|
+
GROUP BY day, version, outcome
|
|
22
|
+
ORDER BY day DESC, version, outcome
|
|
23
|
+
FORMAT JSON
|
|
24
|
+
""".strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "3723bfb3f71e2120e3ed1391fa2ea566")
|
|
29
|
+
token = getpass.getpass("Cloudflare Account Analytics Read token (input hidden): ").strip()
|
|
30
|
+
if not token:
|
|
31
|
+
print("No token provided.", file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
request = Request(
|
|
34
|
+
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql",
|
|
35
|
+
data=QUERY.encode("utf-8"),
|
|
36
|
+
headers={"Authorization": f"Bearer {token}", "Content-Type": "text/plain"},
|
|
37
|
+
method="POST",
|
|
38
|
+
)
|
|
39
|
+
with urlopen(request, timeout=20) as response:
|
|
40
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
41
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.Christofer566/technical-answer-validator",
|
|
4
|
-
"title": "Technical Answer Validator",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "0.1.
|
|
7
|
-
"repository": {
|
|
8
|
-
"url": "https://github.com/Christofer566/technical-answer-validator",
|
|
9
|
-
"source": "github"
|
|
10
|
-
},
|
|
11
|
-
"packages": [
|
|
12
|
-
{
|
|
13
|
-
"registryType": "pypi",
|
|
14
|
-
"registryBaseUrl": "https://pypi.org",
|
|
15
|
-
"identifier": "technical-answer-validator",
|
|
16
|
-
"version": "0.1.
|
|
17
|
-
"transport": { "type": "stdio" }
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
}
|
|
21
|
-
|
|
4
|
+
"title": "Technical Answer Validator",
|
|
5
|
+
"description": "Deterministic rubric-based technical answer feedback for AI agents; review required, not an official grade.",
|
|
6
|
+
"version": "0.1.2",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/Christofer566/technical-answer-validator",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"registryBaseUrl": "https://pypi.org",
|
|
15
|
+
"identifier": "technical-answer-validator",
|
|
16
|
+
"version": "0.1.2",
|
|
17
|
+
"transport": { "type": "stdio" }
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Local-only aggregate usage counts for the MCP stdio server.
|
|
2
|
+
|
|
3
|
+
No request text, rubric, answer, host identity, or network address is stored.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from contextlib import closing
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import sqlite3
|
|
14
|
+
import threading
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
PACKAGE_VERSION = "0.1.2"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _db_path() -> Path:
|
|
23
|
+
configured = os.environ.get("TAV_ANALYTICS_DB")
|
|
24
|
+
if configured:
|
|
25
|
+
return Path(configured)
|
|
26
|
+
return Path.home() / ".technical-answer-validator" / "usage.sqlite3"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _install_id_path() -> Path:
|
|
30
|
+
return Path.home() / ".technical-answer-validator" / "telemetry-id"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _install_id() -> str:
|
|
34
|
+
path = _install_id_path()
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
try:
|
|
37
|
+
value = path.read_text(encoding="ascii").strip()
|
|
38
|
+
uuid.UUID(value)
|
|
39
|
+
return value
|
|
40
|
+
except (OSError, ValueError):
|
|
41
|
+
value = str(uuid.uuid4())
|
|
42
|
+
path.write_text(value, encoding="ascii")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _send_event(outcome: str) -> None:
|
|
47
|
+
"""Best-effort, explicit opt-in remote event; never include tool inputs or outputs."""
|
|
48
|
+
if os.environ.get("TAV_TELEMETRY", "off").lower() not in {"on", "true", "1", "yes"}:
|
|
49
|
+
return
|
|
50
|
+
endpoint = os.environ.get("TAV_TELEMETRY_URL", "").strip()
|
|
51
|
+
if not endpoint.startswith("https://"):
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def send() -> None:
|
|
55
|
+
try:
|
|
56
|
+
payload = json.dumps({
|
|
57
|
+
"installation_id": _install_id(),
|
|
58
|
+
"version": PACKAGE_VERSION,
|
|
59
|
+
"outcome": outcome,
|
|
60
|
+
}).encode("utf-8")
|
|
61
|
+
request = urllib.request.Request(
|
|
62
|
+
endpoint, data=payload,
|
|
63
|
+
headers={"Content-Type": "application/json", "User-Agent": f"technical-answer-validator/{PACKAGE_VERSION}"},
|
|
64
|
+
method="POST",
|
|
65
|
+
)
|
|
66
|
+
with urllib.request.urlopen(request, timeout=2) as response:
|
|
67
|
+
response.read(128)
|
|
68
|
+
except (OSError, urllib.error.URLError, ValueError):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
threading.Thread(target=send, name="tav-telemetry", daemon=True).start()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def record_call(outcome: str) -> None:
|
|
75
|
+
"""Increment the local daily total; analytics can be disabled with TAV_ANALYTICS=off."""
|
|
76
|
+
if outcome not in {"success", "invalid_request", "error"}:
|
|
77
|
+
outcome = "error"
|
|
78
|
+
_send_event(outcome)
|
|
79
|
+
if os.environ.get("TAV_ANALYTICS", "on").lower() in {"off", "false", "0", "no"}:
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
path = _db_path()
|
|
83
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
day = datetime.now(timezone.utc).date().isoformat()
|
|
85
|
+
with closing(sqlite3.connect(path, timeout=5)) as connection:
|
|
86
|
+
with connection:
|
|
87
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS daily_mcp_usage (
|
|
88
|
+
day TEXT PRIMARY KEY,
|
|
89
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
successful INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
invalid_requests INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
errors INTEGER NOT NULL DEFAULT 0
|
|
93
|
+
)""")
|
|
94
|
+
connection.execute("DELETE FROM daily_mcp_usage WHERE day < date('now', '-89 day')")
|
|
95
|
+
connection.execute("INSERT OR IGNORE INTO daily_mcp_usage(day) VALUES(?)", (day,))
|
|
96
|
+
column = {"success": "successful", "invalid_request": "invalid_requests", "error": "errors"}[outcome]
|
|
97
|
+
connection.execute(
|
|
98
|
+
f"UPDATE daily_mcp_usage SET calls=calls+1, {column}={column}+1 WHERE day=?", (day,)
|
|
99
|
+
)
|
|
100
|
+
except (OSError, sqlite3.Error):
|
|
101
|
+
# Local analytics must never make an otherwise valid MCP request fail.
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def report() -> dict[str, object]:
|
|
106
|
+
path = _db_path()
|
|
107
|
+
if not path.exists():
|
|
108
|
+
rows = []
|
|
109
|
+
else:
|
|
110
|
+
with closing(sqlite3.connect(path, timeout=5)) as connection:
|
|
111
|
+
rows = connection.execute(
|
|
112
|
+
"SELECT day,calls,successful,invalid_requests,errors FROM daily_mcp_usage ORDER BY day"
|
|
113
|
+
).fetchall()
|
|
114
|
+
return {
|
|
115
|
+
"source": "local_mcp_stdio",
|
|
116
|
+
"privacy": "local aggregate only; no rubric, answer, host ID, or network address",
|
|
117
|
+
"retention_days": 90,
|
|
118
|
+
"days": [
|
|
119
|
+
{"date_utc": day, "calls": calls, "successful": successful,
|
|
120
|
+
"invalid_requests": invalid_requests, "errors": errors}
|
|
121
|
+
for day, calls, successful, invalid_requests, errors in rows
|
|
122
|
+
],
|
|
123
|
+
"totals": {
|
|
124
|
+
"calls": sum(row[1] for row in rows),
|
|
125
|
+
"successful": sum(row[2] for row in rows),
|
|
126
|
+
"invalid_requests": sum(row[3] for row in rows),
|
|
127
|
+
"errors": sum(row[4] for row in rows),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> None:
|
|
133
|
+
print(json.dumps(report(), ensure_ascii=False, indent=2))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
|
138
|
+
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: technical-answer-validator
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: MCP and REST tools for deterministic rubric-based technical answer review
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: mcp==2.2.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
|
-
# Technical Answer Validator
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<!-- mcp-name: io.github.Christofer566/technical-answer-validator -->
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
A tiny, deterministic answer review tool for AI agents, available over **MCP stdio** and as a REST API. Each caller supplies the concepts, accepted synonyms, numeric requirements, and answer text for a single evaluation. It does not include a question bank or answer corpus.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
This is an **assistive practice tool**, not an official certification exam grader. Keyword matching can miss semantically correct paraphrases and can accept misleading surface matches. Users should review the supplied rubric and every result.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Run locally
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Python 3.10+; the REST server uses the standard library. The MCP adapter uses the official Python SDK 2.2.0.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```powershell
|
|
30
|
-
$env:TAV_API_KEY = "replace-with-a-long-random-secret-at-least-24-characters"
|
|
31
|
-
python -m tav_api
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The service listens on `127.0.0.1:8080` by default. To change it, set `TAV_HOST` and `TAV_PORT`. Local single-key mode refuses to start without a `TAV_API_KEY` of at least 24 characters. For deployment, create a distinct key per caller with `python scripts/create_api_key.py CLIENT_ID` and configure `TAV_API_KEYS` as a JSON object mapping each client ID to the generated SHA-256 digest. Store the one-time raw key with that client; do not store or commit it in this repository. When `TAV_API_KEYS` is set, it takes precedence over `TAV_API_KEY`.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Public release runbook
|
|
2
|
-
|
|
3
|
-
The package contains two delivery paths: local MCP stdio for an agent that can spawn a local process, and a REST API container for hosted agents. No public artifact has been uploaded and no service is deployed by this preparation.
|
|
4
|
-
|
|
5
|
-
## Local MCP package
|
|
6
|
-
|
|
7
|
-
1. MIT was selected and is included in `LICENSE` and the Python package metadata. Confirm that all included source/assets are eligible for MIT before public release.
|
|
8
|
-
2. The public GitHub repository exists at `https://github.com/Christofer566/technical-answer-validator`; source is on `main`. Keep `.venv`, `.uv-cache`, `.env`, usage databases, and generated state out of commits.
|
|
9
|
-
3. Build and inspect the wheel/sdist: `uv build`; test the wheel in a clean virtual environment; `uv run python -m unittest discover -s tests -v`.
|
|
10
|
-
4. PyPI JSON lookup returned HTTP 404 on 2026-10-02. In the PyPI account, register a pending GitHub Actions Trusted Publisher for project `technical-answer-validator`, owner `Christofer566`, repository `technical-answer-validator`, workflow `.github/workflows/publish-pypi.yml`, environment `pypi`. The GitHub `pypi` environment already exists. Publish version `0.1.0` by pushing a `v0.1.0` tag only after ownership/IP approval.
|
|
11
|
-
5. Confirm the built PyPI README contains the exact `mcp-name` marker and that package metadata, repository URL, license, and author are correct.
|
|
12
|
-
6. Validate `server.json` against the current MCP Registry schema and verify the GitHub/PyPI namespace, package name, and version. Then publish with the official MCP Registry CLI using the owner's GitHub identity.
|
|
13
|
-
7. Test installation from the published package in a clean environment with `uvx --from technical-answer-validator tav-mcp`; connect it in target agent hosts and call `evaluate_answer`.
|
|
14
|
-
|
|
15
|
-
## Hosted REST API
|
|
16
|
-
|
|
17
|
-
1. Choose the VPS/host, public domain, TLS reverse proxy, budget ceiling, operator contact, and retention policy. These account/domain choices are not present in the workspace.
|
|
18
|
-
2. Set `TAV_API_KEYS` in the host's secret manager to `{client_id: sha256_hex}` entries generated by `python scripts/create_api_key.py CLIENT_ID`. Deliver each raw key privately to its intended agent operator; only the digest belongs in deployment configuration.
|
|
19
|
-
3. Push an approved `v0.1.0` tag to build and publish `ghcr.io/<owner>/<repo>-api` via `.github/workflows/publish-container.yml`. Pull that image on the host or build directly from the reviewed repository; deploy with `compose.yaml` and retain the `/data` volume. Keep the app bound to loopback behind the HTTPS proxy. Add proxy-level connection/time/body limits and rate limiting; app-level rate buckets reset on restart.
|
|
20
|
-
4. Monitor `/healthz`, per-customer `/v1/usage`, host availability, disk space for SQLite aggregates, and spend. Define key rotation/revocation by removing the corresponding digest and restarting/redeploying.
|
|
21
|
-
5. Verify external HTTPS from a separate network with unauthenticated rejection, valid client isolation, 64 KiB limit, rate limits, no answer logging, and accurate OpenAPI docs before announcing a URL.
|
|
22
|
-
|
|
23
|
-
## Blocks that require owner-controlled decisions/actions
|
|
24
|
-
|
|
25
|
-
- Public source repository exists and `server.json` points to it.
|
|
26
|
-
- PyPI name was not registered at the 2026-10-02 lookup (HTTP 404). PyPI account authentication is required to register its pending publisher; no package has been uploaded.
|
|
27
|
-
- MIT is selected. The owner must still confirm project ownership and third-party rights for included files before publication.
|
|
28
|
-
- No production URL, host account, budget, or service operator contact has been supplied; therefore the container remains local and has not been deployed.
|
|
29
|
-
- The tool's keyword/numeric decisions are not validated against independent subject-matter gold data. Position it as assistive agent feedback, not a trustworthy final grader.
|
|
30
|
-
|
|
31
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Technical Answer Validator
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<!-- mcp-name: io.github.Christofer566/technical-answer-validator -->
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A tiny, deterministic answer review tool for AI agents, available over **MCP stdio** and as a REST API. Each caller supplies the concepts, accepted synonyms, numeric requirements, and answer text for a single evaluation. It does not include a question bank or answer corpus.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
This is an **assistive practice tool**, not an official certification exam grader. Keyword matching can miss semantically correct paraphrases and can accept misleading surface matches. Users should review the supplied rubric and every result.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Run locally
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Python 3.10+; the REST server uses the standard library. The MCP adapter uses the official Python SDK 2.2.0.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```powershell
|
|
20
|
-
$env:TAV_API_KEY = "replace-with-a-long-random-secret-at-least-24-characters"
|
|
21
|
-
python -m tav_api
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
The service listens on `127.0.0.1:8080` by default. To change it, set `TAV_HOST` and `TAV_PORT`. Local single-key mode refuses to start without a `TAV_API_KEY` of at least 24 characters. For deployment, create a distinct key per caller with `python scripts/create_api_key.py CLIENT_ID` and configure `TAV_API_KEYS` as a JSON object mapping each client ID to the generated SHA-256 digest. Store the one-time raw key with that client; do not store or commit it in this repository. When `TAV_API_KEYS` is set, it takes precedence over `TAV_API_KEY`.
|
|
File without changes
|
|
File without changes
|
{technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/claude-mcp-config.example.json
RENAMED
|
File without changes
|
{technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/codex-mcp-config.example.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/scripts/create_api_key.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{technical_answer_validator-0.1.1 → technical_answer_validator-0.1.2}/tests/test_mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|