tdqs 0.1.0__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.
- tdqs-0.1.0/PKG-INFO +134 -0
- tdqs-0.1.0/README.md +103 -0
- tdqs-0.1.0/pyproject.toml +120 -0
- tdqs-0.1.0/pyproject.toml.orig +104 -0
- tdqs-0.1.0/src/tdqs/__init__.py +153 -0
- tdqs-0.1.0/src/tdqs/__main__.py +3 -0
- tdqs-0.1.0/src/tdqs/build_server_coherence_prompt.py +63 -0
- tdqs-0.1.0/src/tdqs/build_tool_scoring_prompt.py +61 -0
- tdqs-0.1.0/src/tdqs/camel_model.py +17 -0
- tdqs-0.1.0/src/tdqs/cli/__init__.py +1 -0
- tdqs-0.1.0/src/tdqs/cli/cli_usage.py +31 -0
- tdqs-0.1.0/src/tdqs/cli/format_lint_report_text.py +41 -0
- tdqs-0.1.0/src/tdqs/cli/format_score_report_text.py +41 -0
- tdqs-0.1.0/src/tdqs/cli/main.py +10 -0
- tdqs-0.1.0/src/tdqs/cli/parse_cli_arguments.py +300 -0
- tdqs-0.1.0/src/tdqs/cli/resolve_tool_definitions.py +43 -0
- tdqs-0.1.0/src/tdqs/cli/run_cli.py +202 -0
- tdqs-0.1.0/src/tdqs/cli/split_command_line.py +67 -0
- tdqs-0.1.0/src/tdqs/compute_context_signals.py +85 -0
- tdqs-0.1.0/src/tdqs/compute_input_hash.py +10 -0
- tdqs-0.1.0/src/tdqs/compute_invocation_cost.py +14 -0
- tdqs-0.1.0/src/tdqs/compute_smells.py +8 -0
- tdqs-0.1.0/src/tdqs/compute_tdqs.py +16 -0
- tdqs-0.1.0/src/tdqs/compute_tier.py +11 -0
- tdqs-0.1.0/src/tdqs/context_signals.py +34 -0
- tdqs-0.1.0/src/tdqs/create_llm_client.py +254 -0
- tdqs-0.1.0/src/tdqs/describe_validation_error.py +12 -0
- tdqs-0.1.0/src/tdqs/evaluate_hard_gates.py +33 -0
- tdqs-0.1.0/src/tdqs/find_shadow_candidates.py +80 -0
- tdqs-0.1.0/src/tdqs/format_lint_report_markdown.py +76 -0
- tdqs-0.1.0/src/tdqs/format_score_report_markdown.py +90 -0
- tdqs-0.1.0/src/tdqs/generated_at.py +9 -0
- tdqs-0.1.0/src/tdqs/lint_report.py +40 -0
- tdqs-0.1.0/src/tdqs/lint_rules.py +83 -0
- tdqs-0.1.0/src/tdqs/lint_server.py +165 -0
- tdqs-0.1.0/src/tdqs/llm_client.py +17 -0
- tdqs-0.1.0/src/tdqs/load_tool_definitions.py +128 -0
- tdqs-0.1.0/src/tdqs/package_version.py +8 -0
- tdqs-0.1.0/src/tdqs/parse_tool_definitions.py +82 -0
- tdqs-0.1.0/src/tdqs/prompts/__init__.py +1 -0
- tdqs-0.1.0/src/tdqs/prompts/server_coherence_system_prompt.py +79 -0
- tdqs-0.1.0/src/tdqs/prompts/tool_scoring_system_prompt.py +91 -0
- tdqs-0.1.0/src/tdqs/py.typed +0 -0
- tdqs-0.1.0/src/tdqs/request_hosted_report.py +104 -0
- tdqs-0.1.0/src/tdqs/rollup_server_score.py +61 -0
- tdqs-0.1.0/src/tdqs/round1.py +11 -0
- tdqs-0.1.0/src/tdqs/round_half_up.py +7 -0
- tdqs-0.1.0/src/tdqs/sanitize_for_prompt.py +17 -0
- tdqs-0.1.0/src/tdqs/score_report.py +20 -0
- tdqs-0.1.0/src/tdqs/score_server.py +75 -0
- tdqs-0.1.0/src/tdqs/score_server_coherence.py +97 -0
- tdqs-0.1.0/src/tdqs/score_tool_definition.py +118 -0
- tdqs-0.1.0/src/tdqs/serialize_tool_definition.py +21 -0
- tdqs-0.1.0/src/tdqs/server_score.py +53 -0
- tdqs-0.1.0/src/tdqs/spec/__init__.py +1 -0
- tdqs-0.1.0/src/tdqs/spec/coherence_dimensions.py +96 -0
- tdqs-0.1.0/src/tdqs/spec/flags.py +84 -0
- tdqs-0.1.0/src/tdqs/spec/spec_version.py +7 -0
- tdqs-0.1.0/src/tdqs/spec/tiers.py +25 -0
- tdqs-0.1.0/src/tdqs/spec/tool_dimensions.py +141 -0
- tdqs-0.1.0/src/tdqs/stringify_json.py +179 -0
- tdqs-0.1.0/src/tdqs/tdqs_input_error.py +4 -0
- tdqs-0.1.0/src/tdqs/tdqs_llm_error.py +17 -0
- tdqs-0.1.0/src/tdqs/tool_definition.py +32 -0
- tdqs-0.1.0/src/tdqs/tool_definitions_source.py +28 -0
- tdqs-0.1.0/src/tdqs/tool_score.py +49 -0
- tdqs-0.1.0/src/tdqs/traverse_required_subtree.py +212 -0
- tdqs-0.1.0/src/tdqs/trim_whitespace.py +13 -0
- tdqs-0.1.0/src/tdqs/utf16_length.py +5 -0
tdqs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tdqs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reference implementation of the Tool Definition Quality Score (TDQS): score how well an MCP tool definition communicates to an AI agent.
|
|
5
|
+
Keywords: lint,mcp,model-context-protocol,quality,tdqs,tool-definition
|
|
6
|
+
Author: Glama
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: anyio>=4.0
|
|
21
|
+
Requires-Dist: exceptiongroup>=1.2 ; python_full_version < '3.11'
|
|
22
|
+
Requires-Dist: httpx>=0.28
|
|
23
|
+
Requires-Dist: mcp>=2.0,<3
|
|
24
|
+
Requires-Dist: pydantic>=2.11
|
|
25
|
+
Requires-Dist: typing-extensions>=4.12
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Project-URL: Homepage, https://tdqs.dev
|
|
28
|
+
Project-URL: Repository, https://github.com/glama-ai/tdqs
|
|
29
|
+
Project-URL: Specification, https://github.com/glama-ai/tool-definition-quality-score
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# tdqs
|
|
33
|
+
|
|
34
|
+
The [Tool Definition Quality Score](https://github.com/glama-ai/tool-definition-quality-score) (TDQS) for Python: a CLI and a library that score how well an MCP tool definition communicates to an AI agent, exactly as the specification defines it. It is the same reference implementation that ships for Node as the `tdqs` npm package, stage for stage, and the two produce the same numbers, the same hashes and the same prompts.
|
|
35
|
+
|
|
36
|
+
TDQS scores a **definition**, not behaviour. The inputs are what an MCP client sees from `tools/list` — name, title, description, input schema, output schema, annotations — and the output is a score from 1.0 to 5.0 with a letter tier, per tool and per server, with a justification for every dimension.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install tdqs
|
|
42
|
+
# or run it without installing
|
|
43
|
+
uvx tdqs --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Python 3.10 or newer.
|
|
47
|
+
|
|
48
|
+
## Lint: deterministic, no model, no key
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tdqs lint --file tools.json
|
|
52
|
+
tdqs lint --command "uvx my-mcp-server"
|
|
53
|
+
tdqs lint --url https://mcp.example.com/mcp --header "Authorization: Bearer …"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`lint` runs the stages of the pipeline that need no model: the context signals (parameter counts, schema description coverage, annotation values, invocation cost, the definition's hash and byte size), the hard gates (no description, tautological description), the shadow prefilter across the tool set, and the checklist the specification ranks highest. It exits `1` on an error-level finding, which makes it a pull request check:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
tdqs lint --file tools.json --fail-on warning --format markdown --output tdqs-lint.md
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A lint finding names a fix. It is not a score, and it never pretends to be one.
|
|
63
|
+
|
|
64
|
+
## Score: the full rubric
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export TDQS_BASE_URL=https://api.openai.com/v1 # any OpenAI-compatible endpoint
|
|
68
|
+
export TDQS_API_KEY=…
|
|
69
|
+
export TDQS_MODEL=…
|
|
70
|
+
|
|
71
|
+
tdqs score --file tools.json
|
|
72
|
+
tdqs score --command "uvx my-mcp-server" --fail-under B --format markdown
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`score` sends every tool through the rubric (six dimensions, 1–5 each, with the specification's system prompt verbatim), runs the server coherence evaluation (four dimensions plus shadowing-risk confirmation), and rolls both up into the server score with integer arithmetic. The report is stamped with the specification version and the model, because a score is calibrated to a rubric+model pair and is not comparable to anything without both.
|
|
76
|
+
|
|
77
|
+
Turn extended reasoning off. The reference model reasons before it answers unless told not to, which makes a call take a minute instead of seconds — and the specification's calibration examples reproduce with reasoning **off**. How to say so is provider-specific, so it is an opaque JSON object merged into every request:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tdqs score --file tools.json --request-overrides '{"reasoning":{"enabled":false}}' # OpenRouter
|
|
81
|
+
# or TDQS_REQUEST_OVERRIDES in the environment; DeepSeek directly takes {"thinking":{"type":"disabled"}}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`--hosted https://tdqs.dev` scores through a hosted TDQS site instead of a model key of your own, and prints the report's URL. It takes that site's API key as `--api-key` or `TDQS_API_KEY`; the site's account page is where keys come from.
|
|
85
|
+
|
|
86
|
+
Input is exactly one of `--file` (a `tools/list` result, an array of tools, or a single tool; `-` reads stdin), `--command` (a stdio server) or `--url` (a Streamable HTTP server).
|
|
87
|
+
|
|
88
|
+
| Exit code | Meaning |
|
|
89
|
+
| --------- | -------------------------------------------------------------------------- |
|
|
90
|
+
| `0` | done |
|
|
91
|
+
| `1` | the threshold was not met (`--fail-on` for lint, `--fail-under` for score) |
|
|
92
|
+
| `2` | usage error, unreadable input, unreachable server, or a model failure |
|
|
93
|
+
|
|
94
|
+
`--format` is `text` (default), `markdown` or `json`. The JSON formats are the ones the npm package publishes as JSON Schema, `schemas/score-report.json` and `schemas/lint-report.json`.
|
|
95
|
+
|
|
96
|
+
## Library
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from tdqs import create_llm_client, lint_server, parse_tool_definitions, score_server
|
|
100
|
+
|
|
101
|
+
parsed = parse_tool_definitions(response.json())
|
|
102
|
+
server_name = parsed.server_name or "my-server"
|
|
103
|
+
|
|
104
|
+
# No model involved.
|
|
105
|
+
lint = lint_server(server_name=server_name, tools=parsed.tools)
|
|
106
|
+
|
|
107
|
+
# The full pipeline.
|
|
108
|
+
report = score_server(
|
|
109
|
+
llm=create_llm_client(
|
|
110
|
+
api_key=api_key,
|
|
111
|
+
base_url=base_url,
|
|
112
|
+
model=model,
|
|
113
|
+
request_overrides={"reasoning": {"enabled": False}},
|
|
114
|
+
),
|
|
115
|
+
server_name=server_name,
|
|
116
|
+
tools=parsed.tools,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
report.server_score.overall_tier # "A" | "B" | "C" | "D" | "F"
|
|
120
|
+
report.tools[0].justifications["usage_guidelines"] # Justification(score=…, justification=…)
|
|
121
|
+
report.model_dump() # the specification's JSON, camelCase keys
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Every stage is exported on its own — `compute_context_signals`, `evaluate_hard_gates`, `compute_tdqs`, `find_shadow_candidates`, `build_tool_scoring_prompt`, `score_tool_definition`, `score_server_coherence`, `rollup_server_score` — along with the specification's metadata (`TOOL_DIMENSIONS`, `COHERENCE_DIMENSIONS`, `FLAGS`, `TIERS`, `LINT_RULES`, `SPEC_VERSION`) and the two system prompts, so a registry or a gateway can build on the same pieces. Reports are pydantic models; `model_dump()` is the specification's JSON and `ScoreReport.model_validate()` reads it back.
|
|
125
|
+
|
|
126
|
+
`request_hosted_report(...)` is the hosted mode as a function: it submits the definitions to a TDQS site and polls until the report is done.
|
|
127
|
+
|
|
128
|
+
## What is deterministic and what is not
|
|
129
|
+
|
|
130
|
+
Stages 1, 2 and 4 of the pipeline, the shadow prefilter, and every rollup are deterministic and reproducible from the definitions alone; `inputHash` is computed the same way the Glama registry computes it, so a hash here matches the one on a server's public score page. Stage 3 — the rubric — and the coherence evaluation are model calls. The specification pins the prompts, the output contract and the calibration examples; the model is the remaining variable, which is why every report names it. Swap models and expect to re-score.
|
|
131
|
+
|
|
132
|
+
## Specification
|
|
133
|
+
|
|
134
|
+
This package follows TDQS **1.2**. The prompts are compared byte for byte against the specification in the test suite, and the deterministic stages are compared against the Node reference implementation's fixtures, so the implementations cannot drift apart silently.
|
tdqs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# tdqs
|
|
2
|
+
|
|
3
|
+
The [Tool Definition Quality Score](https://github.com/glama-ai/tool-definition-quality-score) (TDQS) for Python: a CLI and a library that score how well an MCP tool definition communicates to an AI agent, exactly as the specification defines it. It is the same reference implementation that ships for Node as the `tdqs` npm package, stage for stage, and the two produce the same numbers, the same hashes and the same prompts.
|
|
4
|
+
|
|
5
|
+
TDQS scores a **definition**, not behaviour. The inputs are what an MCP client sees from `tools/list` — name, title, description, input schema, output schema, annotations — and the output is a score from 1.0 to 5.0 with a letter tier, per tool and per server, with a justification for every dimension.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install tdqs
|
|
11
|
+
# or run it without installing
|
|
12
|
+
uvx tdqs --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Python 3.10 or newer.
|
|
16
|
+
|
|
17
|
+
## Lint: deterministic, no model, no key
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
tdqs lint --file tools.json
|
|
21
|
+
tdqs lint --command "uvx my-mcp-server"
|
|
22
|
+
tdqs lint --url https://mcp.example.com/mcp --header "Authorization: Bearer …"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`lint` runs the stages of the pipeline that need no model: the context signals (parameter counts, schema description coverage, annotation values, invocation cost, the definition's hash and byte size), the hard gates (no description, tautological description), the shadow prefilter across the tool set, and the checklist the specification ranks highest. It exits `1` on an error-level finding, which makes it a pull request check:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
tdqs lint --file tools.json --fail-on warning --format markdown --output tdqs-lint.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A lint finding names a fix. It is not a score, and it never pretends to be one.
|
|
32
|
+
|
|
33
|
+
## Score: the full rubric
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export TDQS_BASE_URL=https://api.openai.com/v1 # any OpenAI-compatible endpoint
|
|
37
|
+
export TDQS_API_KEY=…
|
|
38
|
+
export TDQS_MODEL=…
|
|
39
|
+
|
|
40
|
+
tdqs score --file tools.json
|
|
41
|
+
tdqs score --command "uvx my-mcp-server" --fail-under B --format markdown
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`score` sends every tool through the rubric (six dimensions, 1–5 each, with the specification's system prompt verbatim), runs the server coherence evaluation (four dimensions plus shadowing-risk confirmation), and rolls both up into the server score with integer arithmetic. The report is stamped with the specification version and the model, because a score is calibrated to a rubric+model pair and is not comparable to anything without both.
|
|
45
|
+
|
|
46
|
+
Turn extended reasoning off. The reference model reasons before it answers unless told not to, which makes a call take a minute instead of seconds — and the specification's calibration examples reproduce with reasoning **off**. How to say so is provider-specific, so it is an opaque JSON object merged into every request:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
tdqs score --file tools.json --request-overrides '{"reasoning":{"enabled":false}}' # OpenRouter
|
|
50
|
+
# or TDQS_REQUEST_OVERRIDES in the environment; DeepSeek directly takes {"thinking":{"type":"disabled"}}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`--hosted https://tdqs.dev` scores through a hosted TDQS site instead of a model key of your own, and prints the report's URL. It takes that site's API key as `--api-key` or `TDQS_API_KEY`; the site's account page is where keys come from.
|
|
54
|
+
|
|
55
|
+
Input is exactly one of `--file` (a `tools/list` result, an array of tools, or a single tool; `-` reads stdin), `--command` (a stdio server) or `--url` (a Streamable HTTP server).
|
|
56
|
+
|
|
57
|
+
| Exit code | Meaning |
|
|
58
|
+
| --------- | -------------------------------------------------------------------------- |
|
|
59
|
+
| `0` | done |
|
|
60
|
+
| `1` | the threshold was not met (`--fail-on` for lint, `--fail-under` for score) |
|
|
61
|
+
| `2` | usage error, unreadable input, unreachable server, or a model failure |
|
|
62
|
+
|
|
63
|
+
`--format` is `text` (default), `markdown` or `json`. The JSON formats are the ones the npm package publishes as JSON Schema, `schemas/score-report.json` and `schemas/lint-report.json`.
|
|
64
|
+
|
|
65
|
+
## Library
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from tdqs import create_llm_client, lint_server, parse_tool_definitions, score_server
|
|
69
|
+
|
|
70
|
+
parsed = parse_tool_definitions(response.json())
|
|
71
|
+
server_name = parsed.server_name or "my-server"
|
|
72
|
+
|
|
73
|
+
# No model involved.
|
|
74
|
+
lint = lint_server(server_name=server_name, tools=parsed.tools)
|
|
75
|
+
|
|
76
|
+
# The full pipeline.
|
|
77
|
+
report = score_server(
|
|
78
|
+
llm=create_llm_client(
|
|
79
|
+
api_key=api_key,
|
|
80
|
+
base_url=base_url,
|
|
81
|
+
model=model,
|
|
82
|
+
request_overrides={"reasoning": {"enabled": False}},
|
|
83
|
+
),
|
|
84
|
+
server_name=server_name,
|
|
85
|
+
tools=parsed.tools,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
report.server_score.overall_tier # "A" | "B" | "C" | "D" | "F"
|
|
89
|
+
report.tools[0].justifications["usage_guidelines"] # Justification(score=…, justification=…)
|
|
90
|
+
report.model_dump() # the specification's JSON, camelCase keys
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Every stage is exported on its own — `compute_context_signals`, `evaluate_hard_gates`, `compute_tdqs`, `find_shadow_candidates`, `build_tool_scoring_prompt`, `score_tool_definition`, `score_server_coherence`, `rollup_server_score` — along with the specification's metadata (`TOOL_DIMENSIONS`, `COHERENCE_DIMENSIONS`, `FLAGS`, `TIERS`, `LINT_RULES`, `SPEC_VERSION`) and the two system prompts, so a registry or a gateway can build on the same pieces. Reports are pydantic models; `model_dump()` is the specification's JSON and `ScoreReport.model_validate()` reads it back.
|
|
94
|
+
|
|
95
|
+
`request_hosted_report(...)` is the hosted mode as a function: it submits the definitions to a TDQS site and polls until the report is done.
|
|
96
|
+
|
|
97
|
+
## What is deterministic and what is not
|
|
98
|
+
|
|
99
|
+
Stages 1, 2 and 4 of the pipeline, the shadow prefilter, and every rollup are deterministic and reproducible from the definitions alone; `inputHash` is computed the same way the Glama registry computes it, so a hash here matches the one on a server's public score page. Stage 3 — the rubric — and the coherence evaluation are model calls. The specification pins the prompts, the output contract and the calibration examples; the model is the remaining variable, which is why every report names it. Swap models and expect to re-score.
|
|
100
|
+
|
|
101
|
+
## Specification
|
|
102
|
+
|
|
103
|
+
This package follows TDQS **1.2**. The prompts are compared byte for byte against the specification in the test suite, and the deterministic stages are compared against the Node reference implementation's fixtures, so the implementations cannot drift apart silently.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tdqs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Reference implementation of the Tool Definition Quality Score (TDQS): score how well an MCP tool definition communicates to an AI agent."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = [
|
|
9
|
+
"lint",
|
|
10
|
+
"mcp",
|
|
11
|
+
"model-context-protocol",
|
|
12
|
+
"quality",
|
|
13
|
+
"tdqs",
|
|
14
|
+
"tool-definition",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"anyio>=4.0",
|
|
32
|
+
"exceptiongroup>=1.2; python_version < '3.11'",
|
|
33
|
+
"httpx>=0.28",
|
|
34
|
+
"mcp>=2.0,<3",
|
|
35
|
+
"pydantic>=2.11",
|
|
36
|
+
"typing-extensions>=4.12",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[project.authors]]
|
|
40
|
+
name = "Glama"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://tdqs.dev"
|
|
44
|
+
Repository = "https://github.com/glama-ai/tdqs"
|
|
45
|
+
Specification = "https://github.com/glama-ai/tool-definition-quality-score"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
tdqs = "tdqs.cli.main:main"
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
52
|
+
build-backend = "uv_build"
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
dev = [
|
|
56
|
+
"exceptiongroup>=1.2",
|
|
57
|
+
"mypy>=2.0",
|
|
58
|
+
"pytest>=9.0",
|
|
59
|
+
"ruff>=0.16",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = [
|
|
68
|
+
"A",
|
|
69
|
+
"ARG",
|
|
70
|
+
"B",
|
|
71
|
+
"C4",
|
|
72
|
+
"E",
|
|
73
|
+
"ERA",
|
|
74
|
+
"F",
|
|
75
|
+
"I",
|
|
76
|
+
"ISC",
|
|
77
|
+
"N",
|
|
78
|
+
"PERF",
|
|
79
|
+
"PIE",
|
|
80
|
+
"PL",
|
|
81
|
+
"PT",
|
|
82
|
+
"PTH",
|
|
83
|
+
"RET",
|
|
84
|
+
"RUF",
|
|
85
|
+
"SIM",
|
|
86
|
+
"TRY",
|
|
87
|
+
"UP",
|
|
88
|
+
"W",
|
|
89
|
+
]
|
|
90
|
+
ignore = [
|
|
91
|
+
"E501",
|
|
92
|
+
"PLR0911",
|
|
93
|
+
"PLR0912",
|
|
94
|
+
"PLR0913",
|
|
95
|
+
"PLR0915",
|
|
96
|
+
"PLR2004",
|
|
97
|
+
"RUF001",
|
|
98
|
+
"RUF002",
|
|
99
|
+
"RUF003",
|
|
100
|
+
"TRY003",
|
|
101
|
+
"TRY301",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[tool.ruff.lint.per-file-ignores]
|
|
105
|
+
"tests/*" = [
|
|
106
|
+
"PLR0917",
|
|
107
|
+
"PLR2004",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[tool.mypy]
|
|
111
|
+
files = [
|
|
112
|
+
"src",
|
|
113
|
+
"tests",
|
|
114
|
+
]
|
|
115
|
+
plugins = ["pydantic.mypy"]
|
|
116
|
+
python_version = "3.10"
|
|
117
|
+
strict = true
|
|
118
|
+
|
|
119
|
+
[tool.pytest.ini_options]
|
|
120
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tdqs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Reference implementation of the Tool Definition Quality Score (TDQS): score how well an MCP tool definition communicates to an AI agent."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "Glama" }]
|
|
9
|
+
keywords = ["lint", "mcp", "model-context-protocol", "quality", "tdqs", "tool-definition"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
# `mcp` is the official client, used to read `tools/list` from a live server; `httpx` and
|
|
25
|
+
# `pydantic` are what it is built on, declared here because this package imports them directly.
|
|
26
|
+
dependencies = [
|
|
27
|
+
"anyio>=4.0",
|
|
28
|
+
"exceptiongroup>=1.2; python_version < '3.11'",
|
|
29
|
+
"httpx>=0.28",
|
|
30
|
+
"mcp>=2.0,<3",
|
|
31
|
+
"pydantic>=2.11",
|
|
32
|
+
"typing-extensions>=4.12",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://tdqs.dev"
|
|
37
|
+
Repository = "https://github.com/glama-ai/tdqs"
|
|
38
|
+
Specification = "https://github.com/glama-ai/tool-definition-quality-score"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
tdqs = "tdqs.cli.main:main"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
# `exceptiongroup` is only a runtime dependency below Python 3.11, but mypy checks that branch
|
|
49
|
+
# on every interpreter and needs its types.
|
|
50
|
+
dev = ["exceptiongroup>=1.2", "mypy>=2.0", "pytest>=9.0", "ruff>=0.16"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = [
|
|
58
|
+
"A", # shadowed builtins
|
|
59
|
+
"ARG", # unused arguments
|
|
60
|
+
"B", # bugbear
|
|
61
|
+
"C4", # comprehensions
|
|
62
|
+
"E", # pycodestyle errors
|
|
63
|
+
"ERA", # commented-out code
|
|
64
|
+
"F", # pyflakes
|
|
65
|
+
"I", # import order
|
|
66
|
+
"ISC", # implicit string concatenation
|
|
67
|
+
"N", # naming
|
|
68
|
+
"PERF", # performance
|
|
69
|
+
"PIE", # misc lints
|
|
70
|
+
"PL", # pylint
|
|
71
|
+
"PT", # pytest style
|
|
72
|
+
"PTH", # pathlib
|
|
73
|
+
"RET", # return statements
|
|
74
|
+
"RUF", # ruff's own
|
|
75
|
+
"SIM", # simplification
|
|
76
|
+
"TRY", # exception handling
|
|
77
|
+
"UP", # modern syntax
|
|
78
|
+
"W", # pycodestyle warnings
|
|
79
|
+
]
|
|
80
|
+
ignore = [
|
|
81
|
+
"E501", # line length: the formatter lays code out, and the specification's strings are long
|
|
82
|
+
"PLR0911", # too many return statements: the parsers are dispatch tables
|
|
83
|
+
"PLR0912", # too many branches: the CLI parser and the schema traversal are long by nature
|
|
84
|
+
"PLR0913", # too many arguments: keyword-only signatures are the interface, like the TS package
|
|
85
|
+
"PLR0915", # too many statements
|
|
86
|
+
"PLR2004", # magic values: the specification's constants are documented where they are used
|
|
87
|
+
"RUF001", # ambiguous unicode: the specification's own text uses dashes and arrows, verbatim
|
|
88
|
+
"RUF002",
|
|
89
|
+
"RUF003",
|
|
90
|
+
"TRY003", # long exception messages: the messages are the interface
|
|
91
|
+
"TRY301", # raise inside try: the CLI raises usage errors where the usage handler catches them
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.per-file-ignores]
|
|
95
|
+
"tests/*" = ["PLR0917", "PLR2004"]
|
|
96
|
+
|
|
97
|
+
[tool.mypy]
|
|
98
|
+
files = ["src", "tests"]
|
|
99
|
+
plugins = ["pydantic.mypy"]
|
|
100
|
+
python_version = "3.10"
|
|
101
|
+
strict = true
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The reference implementation of the Tool Definition Quality Score, for Python.
|
|
2
|
+
|
|
3
|
+
The package's public surface. The modules are one export each; this is the one stable entry
|
|
4
|
+
point, so `from tdqs import score_server` keeps working when a module moves.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tdqs.build_server_coherence_prompt import CoherencePromptTool, build_server_coherence_prompt
|
|
8
|
+
from tdqs.build_tool_scoring_prompt import build_tool_scoring_prompt
|
|
9
|
+
from tdqs.compute_context_signals import compute_context_signals
|
|
10
|
+
from tdqs.compute_input_hash import compute_input_hash
|
|
11
|
+
from tdqs.compute_invocation_cost import compute_invocation_cost
|
|
12
|
+
from tdqs.compute_smells import compute_smells
|
|
13
|
+
from tdqs.compute_tdqs import compute_tdqs
|
|
14
|
+
from tdqs.compute_tier import compute_tier
|
|
15
|
+
from tdqs.context_signals import AnnotationValues, ContextSignals
|
|
16
|
+
from tdqs.create_llm_client import OpenAiCompatibleLlmClient, create_llm_client
|
|
17
|
+
from tdqs.evaluate_hard_gates import HardGateResult, evaluate_hard_gates
|
|
18
|
+
from tdqs.find_shadow_candidates import CostedTool, ShadowCandidate, find_shadow_candidates
|
|
19
|
+
from tdqs.format_lint_report_markdown import format_lint_report_markdown
|
|
20
|
+
from tdqs.format_score_report_markdown import format_score_report_markdown
|
|
21
|
+
from tdqs.lint_report import LintedTool, LintFinding, LintReport, ServerSummary
|
|
22
|
+
from tdqs.lint_rules import LINT_RULES, LintRule, LintRuleKey, LintSeverity
|
|
23
|
+
from tdqs.lint_server import lint_server
|
|
24
|
+
from tdqs.llm_client import LlmClient
|
|
25
|
+
from tdqs.load_tool_definitions import load_tool_definitions
|
|
26
|
+
from tdqs.package_version import PACKAGE_VERSION
|
|
27
|
+
from tdqs.parse_tool_definitions import ParsedToolDefinitions, parse_tool_definitions
|
|
28
|
+
from tdqs.prompts.server_coherence_system_prompt import SERVER_COHERENCE_SYSTEM_PROMPT
|
|
29
|
+
from tdqs.prompts.tool_scoring_system_prompt import TOOL_SCORING_SYSTEM_PROMPT
|
|
30
|
+
from tdqs.request_hosted_report import HostedReport, HostedReportStatus, request_hosted_report
|
|
31
|
+
from tdqs.rollup_server_score import rollup_server_score
|
|
32
|
+
from tdqs.round1 import round1
|
|
33
|
+
from tdqs.score_report import ScoreReport
|
|
34
|
+
from tdqs.score_server import score_server
|
|
35
|
+
from tdqs.score_server_coherence import ServerCoherence, score_server_coherence
|
|
36
|
+
from tdqs.score_tool_definition import score_tool_definition
|
|
37
|
+
from tdqs.serialize_tool_definition import serialize_tool_definition
|
|
38
|
+
from tdqs.server_score import (
|
|
39
|
+
CoherenceJustifications,
|
|
40
|
+
CoherenceScores,
|
|
41
|
+
ServerScore,
|
|
42
|
+
ShadowingRisk,
|
|
43
|
+
)
|
|
44
|
+
from tdqs.spec.coherence_dimensions import (
|
|
45
|
+
COHERENCE_DIMENSIONS,
|
|
46
|
+
CoherenceDimension,
|
|
47
|
+
CoherenceDimensionKey,
|
|
48
|
+
)
|
|
49
|
+
from tdqs.spec.flags import FLAGS, Flag, HardGateFlag, ServerFlag, ToolFlag
|
|
50
|
+
from tdqs.spec.spec_version import SPEC_VERSION
|
|
51
|
+
from tdqs.spec.tiers import TIERS, Tier, TierDefinition
|
|
52
|
+
from tdqs.spec.tool_dimensions import (
|
|
53
|
+
TOOL_DIMENSIONS,
|
|
54
|
+
DimensionScore,
|
|
55
|
+
ToolDimension,
|
|
56
|
+
ToolDimensionKey,
|
|
57
|
+
)
|
|
58
|
+
from tdqs.stringify_json import stringify_json
|
|
59
|
+
from tdqs.tdqs_input_error import TdqsInputError
|
|
60
|
+
from tdqs.tdqs_llm_error import TdqsLlmError
|
|
61
|
+
from tdqs.tool_definition import ToolDefinition
|
|
62
|
+
from tdqs.tool_definitions_source import (
|
|
63
|
+
CommandSource,
|
|
64
|
+
FileSource,
|
|
65
|
+
ToolDefinitionsSource,
|
|
66
|
+
UrlSource,
|
|
67
|
+
)
|
|
68
|
+
from tdqs.tool_score import DimensionJustifications, DimensionScores, Justification, ToolScore
|
|
69
|
+
from tdqs.traverse_required_subtree import RequiredSubtreeSignals, traverse_required_subtree
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"COHERENCE_DIMENSIONS",
|
|
73
|
+
"FLAGS",
|
|
74
|
+
"LINT_RULES",
|
|
75
|
+
"PACKAGE_VERSION",
|
|
76
|
+
"SERVER_COHERENCE_SYSTEM_PROMPT",
|
|
77
|
+
"SPEC_VERSION",
|
|
78
|
+
"TIERS",
|
|
79
|
+
"TOOL_DIMENSIONS",
|
|
80
|
+
"TOOL_SCORING_SYSTEM_PROMPT",
|
|
81
|
+
"AnnotationValues",
|
|
82
|
+
"CoherenceDimension",
|
|
83
|
+
"CoherenceDimensionKey",
|
|
84
|
+
"CoherenceJustifications",
|
|
85
|
+
"CoherencePromptTool",
|
|
86
|
+
"CoherenceScores",
|
|
87
|
+
"CommandSource",
|
|
88
|
+
"ContextSignals",
|
|
89
|
+
"CostedTool",
|
|
90
|
+
"DimensionJustifications",
|
|
91
|
+
"DimensionScore",
|
|
92
|
+
"DimensionScores",
|
|
93
|
+
"FileSource",
|
|
94
|
+
"Flag",
|
|
95
|
+
"HardGateFlag",
|
|
96
|
+
"HardGateResult",
|
|
97
|
+
"HostedReport",
|
|
98
|
+
"HostedReportStatus",
|
|
99
|
+
"Justification",
|
|
100
|
+
"LintFinding",
|
|
101
|
+
"LintReport",
|
|
102
|
+
"LintRule",
|
|
103
|
+
"LintRuleKey",
|
|
104
|
+
"LintSeverity",
|
|
105
|
+
"LintedTool",
|
|
106
|
+
"LlmClient",
|
|
107
|
+
"OpenAiCompatibleLlmClient",
|
|
108
|
+
"ParsedToolDefinitions",
|
|
109
|
+
"RequiredSubtreeSignals",
|
|
110
|
+
"ScoreReport",
|
|
111
|
+
"ServerCoherence",
|
|
112
|
+
"ServerFlag",
|
|
113
|
+
"ServerScore",
|
|
114
|
+
"ServerSummary",
|
|
115
|
+
"ShadowCandidate",
|
|
116
|
+
"ShadowingRisk",
|
|
117
|
+
"TdqsInputError",
|
|
118
|
+
"TdqsLlmError",
|
|
119
|
+
"Tier",
|
|
120
|
+
"TierDefinition",
|
|
121
|
+
"ToolDefinition",
|
|
122
|
+
"ToolDefinitionsSource",
|
|
123
|
+
"ToolDimension",
|
|
124
|
+
"ToolDimensionKey",
|
|
125
|
+
"ToolFlag",
|
|
126
|
+
"ToolScore",
|
|
127
|
+
"UrlSource",
|
|
128
|
+
"build_server_coherence_prompt",
|
|
129
|
+
"build_tool_scoring_prompt",
|
|
130
|
+
"compute_context_signals",
|
|
131
|
+
"compute_input_hash",
|
|
132
|
+
"compute_invocation_cost",
|
|
133
|
+
"compute_smells",
|
|
134
|
+
"compute_tdqs",
|
|
135
|
+
"compute_tier",
|
|
136
|
+
"create_llm_client",
|
|
137
|
+
"evaluate_hard_gates",
|
|
138
|
+
"find_shadow_candidates",
|
|
139
|
+
"format_lint_report_markdown",
|
|
140
|
+
"format_score_report_markdown",
|
|
141
|
+
"lint_server",
|
|
142
|
+
"load_tool_definitions",
|
|
143
|
+
"parse_tool_definitions",
|
|
144
|
+
"request_hosted_report",
|
|
145
|
+
"rollup_server_score",
|
|
146
|
+
"round1",
|
|
147
|
+
"score_server",
|
|
148
|
+
"score_server_coherence",
|
|
149
|
+
"score_tool_definition",
|
|
150
|
+
"serialize_tool_definition",
|
|
151
|
+
"stringify_json",
|
|
152
|
+
"traverse_required_subtree",
|
|
153
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from tdqs.context_signals import ContextSignals
|
|
5
|
+
from tdqs.find_shadow_candidates import ShadowCandidate
|
|
6
|
+
from tdqs.sanitize_for_prompt import sanitize_for_prompt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CoherencePromptTool:
|
|
11
|
+
context_signals: ContextSignals
|
|
12
|
+
description: str | None
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_server_coherence_prompt(
|
|
17
|
+
*,
|
|
18
|
+
server_name: str,
|
|
19
|
+
shadow_candidates: Sequence[ShadowCandidate],
|
|
20
|
+
tools: Sequence[CoherencePromptTool],
|
|
21
|
+
) -> str:
|
|
22
|
+
"""The user message of the coherence call, in the specification's template: every tool with
|
|
23
|
+
its invocation cost and the three signals behind it, then the candidate pairs the prefilter
|
|
24
|
+
emitted. The model is told to judge only those pairs, which is why they are spelled out rather
|
|
25
|
+
than left for it to find."""
|
|
26
|
+
tool_lines: list[str] = []
|
|
27
|
+
|
|
28
|
+
for tool in tools:
|
|
29
|
+
signals = tool.context_signals
|
|
30
|
+
cost = (
|
|
31
|
+
f"cost {signals.invocation_cost}: {signals.required_field_count} required, "
|
|
32
|
+
f"depth {signals.schema_depth}, {signals.union_choice_count} union choices"
|
|
33
|
+
)
|
|
34
|
+
text = (
|
|
35
|
+
"(no description)"
|
|
36
|
+
if tool.description is None
|
|
37
|
+
else str(sanitize_for_prompt(tool.description))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
tool_lines.append(f"- {tool.name} [{cost}]: {text}")
|
|
41
|
+
|
|
42
|
+
candidate_lines = [
|
|
43
|
+
f"{candidate.tool} (cost {candidate.invocation_cost}) may be shadowed by "
|
|
44
|
+
f"{candidate.cheaper_sibling} (cost {candidate.cheaper_sibling_invocation_cost})"
|
|
45
|
+
for candidate in shadow_candidates
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return "\n".join(
|
|
49
|
+
[
|
|
50
|
+
f"SERVER NAME: {sanitize_for_prompt(server_name)}",
|
|
51
|
+
f"TOOL COUNT: {len(tools)}",
|
|
52
|
+
"",
|
|
53
|
+
"<tools>",
|
|
54
|
+
*tool_lines,
|
|
55
|
+
"</tools>",
|
|
56
|
+
"",
|
|
57
|
+
"<shadow-candidates>",
|
|
58
|
+
"\n".join(candidate_lines) if len(candidate_lines) > 0 else "None",
|
|
59
|
+
"</shadow-candidates>",
|
|
60
|
+
"",
|
|
61
|
+
"Respond with JSON only.",
|
|
62
|
+
]
|
|
63
|
+
)
|