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.
Files changed (69) hide show
  1. tdqs-0.1.0/PKG-INFO +134 -0
  2. tdqs-0.1.0/README.md +103 -0
  3. tdqs-0.1.0/pyproject.toml +120 -0
  4. tdqs-0.1.0/pyproject.toml.orig +104 -0
  5. tdqs-0.1.0/src/tdqs/__init__.py +153 -0
  6. tdqs-0.1.0/src/tdqs/__main__.py +3 -0
  7. tdqs-0.1.0/src/tdqs/build_server_coherence_prompt.py +63 -0
  8. tdqs-0.1.0/src/tdqs/build_tool_scoring_prompt.py +61 -0
  9. tdqs-0.1.0/src/tdqs/camel_model.py +17 -0
  10. tdqs-0.1.0/src/tdqs/cli/__init__.py +1 -0
  11. tdqs-0.1.0/src/tdqs/cli/cli_usage.py +31 -0
  12. tdqs-0.1.0/src/tdqs/cli/format_lint_report_text.py +41 -0
  13. tdqs-0.1.0/src/tdqs/cli/format_score_report_text.py +41 -0
  14. tdqs-0.1.0/src/tdqs/cli/main.py +10 -0
  15. tdqs-0.1.0/src/tdqs/cli/parse_cli_arguments.py +300 -0
  16. tdqs-0.1.0/src/tdqs/cli/resolve_tool_definitions.py +43 -0
  17. tdqs-0.1.0/src/tdqs/cli/run_cli.py +202 -0
  18. tdqs-0.1.0/src/tdqs/cli/split_command_line.py +67 -0
  19. tdqs-0.1.0/src/tdqs/compute_context_signals.py +85 -0
  20. tdqs-0.1.0/src/tdqs/compute_input_hash.py +10 -0
  21. tdqs-0.1.0/src/tdqs/compute_invocation_cost.py +14 -0
  22. tdqs-0.1.0/src/tdqs/compute_smells.py +8 -0
  23. tdqs-0.1.0/src/tdqs/compute_tdqs.py +16 -0
  24. tdqs-0.1.0/src/tdqs/compute_tier.py +11 -0
  25. tdqs-0.1.0/src/tdqs/context_signals.py +34 -0
  26. tdqs-0.1.0/src/tdqs/create_llm_client.py +254 -0
  27. tdqs-0.1.0/src/tdqs/describe_validation_error.py +12 -0
  28. tdqs-0.1.0/src/tdqs/evaluate_hard_gates.py +33 -0
  29. tdqs-0.1.0/src/tdqs/find_shadow_candidates.py +80 -0
  30. tdqs-0.1.0/src/tdqs/format_lint_report_markdown.py +76 -0
  31. tdqs-0.1.0/src/tdqs/format_score_report_markdown.py +90 -0
  32. tdqs-0.1.0/src/tdqs/generated_at.py +9 -0
  33. tdqs-0.1.0/src/tdqs/lint_report.py +40 -0
  34. tdqs-0.1.0/src/tdqs/lint_rules.py +83 -0
  35. tdqs-0.1.0/src/tdqs/lint_server.py +165 -0
  36. tdqs-0.1.0/src/tdqs/llm_client.py +17 -0
  37. tdqs-0.1.0/src/tdqs/load_tool_definitions.py +128 -0
  38. tdqs-0.1.0/src/tdqs/package_version.py +8 -0
  39. tdqs-0.1.0/src/tdqs/parse_tool_definitions.py +82 -0
  40. tdqs-0.1.0/src/tdqs/prompts/__init__.py +1 -0
  41. tdqs-0.1.0/src/tdqs/prompts/server_coherence_system_prompt.py +79 -0
  42. tdqs-0.1.0/src/tdqs/prompts/tool_scoring_system_prompt.py +91 -0
  43. tdqs-0.1.0/src/tdqs/py.typed +0 -0
  44. tdqs-0.1.0/src/tdqs/request_hosted_report.py +104 -0
  45. tdqs-0.1.0/src/tdqs/rollup_server_score.py +61 -0
  46. tdqs-0.1.0/src/tdqs/round1.py +11 -0
  47. tdqs-0.1.0/src/tdqs/round_half_up.py +7 -0
  48. tdqs-0.1.0/src/tdqs/sanitize_for_prompt.py +17 -0
  49. tdqs-0.1.0/src/tdqs/score_report.py +20 -0
  50. tdqs-0.1.0/src/tdqs/score_server.py +75 -0
  51. tdqs-0.1.0/src/tdqs/score_server_coherence.py +97 -0
  52. tdqs-0.1.0/src/tdqs/score_tool_definition.py +118 -0
  53. tdqs-0.1.0/src/tdqs/serialize_tool_definition.py +21 -0
  54. tdqs-0.1.0/src/tdqs/server_score.py +53 -0
  55. tdqs-0.1.0/src/tdqs/spec/__init__.py +1 -0
  56. tdqs-0.1.0/src/tdqs/spec/coherence_dimensions.py +96 -0
  57. tdqs-0.1.0/src/tdqs/spec/flags.py +84 -0
  58. tdqs-0.1.0/src/tdqs/spec/spec_version.py +7 -0
  59. tdqs-0.1.0/src/tdqs/spec/tiers.py +25 -0
  60. tdqs-0.1.0/src/tdqs/spec/tool_dimensions.py +141 -0
  61. tdqs-0.1.0/src/tdqs/stringify_json.py +179 -0
  62. tdqs-0.1.0/src/tdqs/tdqs_input_error.py +4 -0
  63. tdqs-0.1.0/src/tdqs/tdqs_llm_error.py +17 -0
  64. tdqs-0.1.0/src/tdqs/tool_definition.py +32 -0
  65. tdqs-0.1.0/src/tdqs/tool_definitions_source.py +28 -0
  66. tdqs-0.1.0/src/tdqs/tool_score.py +49 -0
  67. tdqs-0.1.0/src/tdqs/traverse_required_subtree.py +212 -0
  68. tdqs-0.1.0/src/tdqs/trim_whitespace.py +13 -0
  69. 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,3 @@
1
+ from tdqs.cli.main import main
2
+
3
+ main()
@@ -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
+ )