DEV Community

SAI RAM
SAI RAM

Posted on Originally published at anvilry.vercel.app

tracehub-mcp v0.12.3 and cost-guard-mcp v0.3.3: Two MCP Servers Shipped the Same Morning, and the Metadata Layer That Had to Learn to Tell the Truth

Two MCP servers from the same GitHub org, mcpsmiths, went out to PyPI just under twelve minutes apart on the morning of 2026-10-10. cost-guard-mcp v0.3.3's wheel landed at 11:01:04Z; tracehub-mcp v0.12.3's at 11:12:53Z. Both went through OIDC Trusted Publishing, both carry PEP 740 attestations, and both were the first tag after a long quiet stretch. I wrote about each project once before, on 2026-09-13, when tracehub-mcp was at v0.3.0 and cost-guard-mcp was at v0.1.1. Those articles were independent. This one is not, because when I drafted the two release posts separately they converged on the same sentence: the code shipped correctly, and what kept lagging was the layer that describes the code to everything outside the repository.

That layer has a lot of files in it: server.json, which the official MCP Registry reads; pyproject.toml keywords and classifiers, which PyPI serves and directories like mcprush import; mcpb/manifest.json; GitHub topics; the README's install block; the release body; the uv.lock version string. None of it changes what the server does when a client calls tools/list, and almost none of it was covered by a test in either project a month ago. Across the two spans I count a committed server.json wrong at eleven consecutive tags, two release runs that published to PyPI and then died on a 100-character registry limit, four container images whose Dockerfile stages disagreed on the Python version, a registry entry stuck a version behind because one pipeline has no registry step, two directory listings that sat at the previous version for the whole gap and moved only when the publisher forced a re-read today, a README advertising a packaging extra that does not exist, and a patch pair twenty-two minutes apart that changed zero lines of source.

The two projects are at different points on that curve, and the contrast is the article. tracehub-mcp spent its last twenty days building tests and pipeline gates against exactly this class of drift, and every defect in its list now has a guard behind it, though one guard has never been exercised. cost-guard-mcp fixed the defects that make the number it reports honest, signed its artifacts, and still has most of its discoverability gaps open today. If you only read one section, read the "Where to get them" tables; that is where the gap is visible in a form you can check yourself.

Every clock time below is UTC unless marked IST. File:line references are against each repo's HEAD today: tracehub-mcp 89659c7, cost-guard-mcp 84ec45a. Facts about one project are labelled as that project's throughout; nothing is shared between them unless I say so.

Where each story left off

tracehub-mcp's previous article ended at v0.3.0: five backends, eleven MCP tools, and an argument about which parts of the code get held to a security bar. What it did not mention is that v0.4.0 was tagged the same day, six hours and twenty-four minutes later, with Origin-header validation on the HTTP transport and read-only ToolAnnotations on every tool. That release never got written up, so this span opens at v0.4.0. Between v0.4.0 and v0.12.3 sit 126 commits and fourteen tags. Twelve of the thirteen tags after v0.4.0 landed in seven days, 2026-09-14 through 2026-09-20, then nothing for twenty days until today.

cost-guard-mcp's previous article left it at v0.1.1: two engines (BigQuery PRECISE, Snowflake UPPER_BOUND), three tools, and HEURISTIC as a type-level placeholder with no Databricks code behind it. Five tags since: v0.2.0 (2026-09-17), v0.3.0 (2026-09-18), v0.3.1 and v0.3.2 (2026-09-22, twenty-two minutes apart), and v0.3.3, tagged today at 11:00Z and on PyPI a minute later. Engines went 2 to 3, tools 3 to 4, the unit suite from 65 tests to 218 at 99% coverage. The sentence that opens changelog/v0.2.0.md sets the tone: "The published 0.1.1 package on PyPI never actually contained any of the work below." That is the changelog's admission, not mine; I have not diffed the wheel.

tracehub-mcp: twelve releases in seven days, and what leaked out of each one

The burst took the surface from 5 backends and 11 tools to 8 backends and 19 tools. AWS X-Ray landed in v0.11.0; New Relic and Honeycomb landed together in v0.12.0. Four analysis tools arrived in v0.5.0, two investigation tools in v0.7.0, two trace-reasoning tools in v0.12.0. Test files went from 26 at v0.4.0 to 51 at v0.12.3, 52 on main today; uv run pytest -q --no-cov on HEAD reports 1173 passed, 2 skipped, in 33.8 s.

Every one of those expansions leaked through a seam the tests did not cover, and the seam was never the code. Three consecutive backend additions each drifted a different file: server.json, then mcpb/manifest.json, then pyproject.toml keywords and GitHub topics.

tracehub-mcp: three new backends, built from docs rather than accounts

Every backend implements the BaseBackend contract in src/opentelemetry_mcp/backends/base.py:219-389, and the backend Literal at config.py:21-23 is mirrored in four other places: attributes.py:366-368, the click.Choice at server.py:1537-1539, server.json:25 and mcpb/manifest.json:80. Those mirrors are where the drift happened.

AWS X-Ray (v0.11.0, xray.py, 915 lines, 63 tests). No API key; a boto3.client("xray", ...) with standard-mode retries (xray.py:173-178), credentials from the boto3 default chain, and BACKEND_AWS_REGION required with no default (xray.py:166-170). The design decision that matters is filter pushdown: _split_filters only pushes a filter when its field is in _NATIVE_TRACE_LEVEL_FIELDS = {service.name, duration, status} (xray.py:111, 457-471); everything under gen_ai.* is filtered client-side after hydration, because OTel attributes become unindexed X-Ray segment metadata and pushing annotation.<key> would silently return zero whenever the customer's ADOT config does not index that key (xray.py:29-40). Tests use botocore.stub.Stubber, so every request is validated against the real service model. The docstring at xray.py:7-13 says plainly that this was built with no live AWS account.

New Relic via NerdGraph (v0.12.0, newrelic.py, 1052 lines, 77 tests). BACKEND_API_KEY must be a User key, sent as the non-standard API-Key header; BACKEND_NEWRELIC_ACCOUNT_ID is required. https-only at newrelic.py:213-217, a check the pre-landing review had to add because the docstring argued for it and the code did not enforce it. An asyncio.Semaphore(20) sits under New Relic's documented 25-concurrent ceiling. search_traces hydrates up to 50 trace IDs (newrelic.py:343-349), then overlays attributes and status from the flat NRQL rows, because the shape of distributedTracing...spans[].attributes is unverified (newrelic.py:22-31); the first implementation discarded the rows entirely before review caught it (a41fa62).

Honeycomb via the Query Data API (v0.12.0, honeycomb.py, 1090 lines, 67 tests). Building a query spec is ungated; running it via POST /1/query_results/{dataset} is documented Enterprise-only (honeycomb.py:3-8, 17-26), and a 402 or 403 from either step raises HoneycombEnterpriseRequiredError. Health is GET /1/auth, which avoids the gated path, so a Free or Pro account will report healthy and then fail every query. search_traces batches every discovered trace ID into one trace.trace_id IN (...) query (honeycomb.py:309-385), because _RateLimiter enforces _QUERY_RESULT_RATE_LIMIT_PER_MINUTE = 10.

All three module docstrings enumerate exactly what is unverified, and the README's Backend Support Matrix (README.md:617-633) footnotes each caveat. That is the opposite of how Datadog and Sentry ended up.

A hand reaching into a wall-sized telephone switchboard crowded with dozens of criss-crossing patch cords

tracehub-mcp: what a live account revealed about Datadog and Sentry

The Datadog and Sentry backends I described last time were docs-grounded. v0.6.0 ran them against real trial accounts and found both had been silently wrong since they shipped. Datadog's Spans API v2 nests OTel attributes under attributes.custom; the code read attributes.attributes, a key that does not exist, so every gen_ai-dependent tool had been returning empty results against any real Datadog account, and every fixture matched the same wrong assumption (e956a5b). The fix is attrs.get("custom") at datadog.py:742. Sentry's get_trace() rejected every item, because GET /organizations/{org}/trace/{trace_id}/ returns items that never carry trace_id; the fix trusts requested_trace_id (sentry.py:1186-1196). The same release handled the OTel semconv v1.37.0 rename of gen_ai.system to gen_ai.provider.name, which had been landing silently in extra="allow" and making spans vanish from every LLM tool; _normalize_gen_ai_provider_rename at attributes.py:97-128 rewrites it back. One inconsistency to state rather than echo: sentry.py:14-20 still says no live account, while inline comments at sentry.py:297 and :1186 say a live account confirmed the behaviour.

tracehub-mcp: the tool surface, 11 to 19

Counting ^@mcp\.tool in server.py per tag: 11 at v0.4.0, 15 at v0.5.0, 17 at v0.7.0, 19 at v0.12.0. Nothing was removed or renamed. Two wire names look like mistakes and are not: no decorator passes name=, so FastMCP exposes the Python function name, and two of the nineteen are search_spans_tool and list_llm_tools_tool because server.py:48-66 imports modules named search_spans and list_llm_tools. An in-memory fastmcp.Client(mcp).list_tools() confirms exactly 19, equal to _ALL_TOOL_NAMES at server.py:1190-1212.

Of the additions, triage_trace (v0.12.0) is deterministic with no LLM call: it walks every error chain and names the last span of the deepest chain as likely_root_cause, with "low" confidence when error spans sit in disconnected or cyclic islands, which real backends produce via self-parented spans (a v0.12.1 fix; every walk in span_tree.py is cycle-safe). correlate_trace (v0.12.0) needs an opt-in second backend and is not a schema join, because W3C Trace Context does not guarantee trace-id continuity across vendors. v0.5.0 adopted SEP-2140 error semantics, with _handle_tool_error (server.py:87-98) typed -> NoReturn so the SDK reports CallToolResult(isError=True). Six of nineteen tools have real outputSchema; thirteen return str inside FastMCP's x-fastmcp-wrap-result wrapper. Every tool carries read-only ToolAnnotations plus a title=, pinned by tests/test_server.py:784-812.

tracehub-mcp: the discoverability drift, with receipts

Stage one, server.json. Run git show <tag>:server.json for every tag and read .version: 0.2.3 at v0.3.0 and v0.4.0; 0.4.0 at eleven consecutive tags, v0.5.0 through v0.12.1; 0.12.1 at v0.12.2. The committed file was behind at every tag that had one, right up to today. Nobody saw it because mcp-registry-publish.yml:62-70 jq-rewrites both version and packages[0].version at publish time, so the registry always received the right number even when the repo did not hold it. The choices list had no such rewrite. BACKEND_TYPE.choices at v0.12.1 had six of eight backends, and the live registry entries for 0.12.0 and 0.12.1 still end "...Sentry, X-Ray.", text that was accurate for 0.11.0 and wrong for the two after it. Commit a465825 fixed the choices in v0.12.2, which pushed the description to 123 characters, and the v0.12.2 release run (35526299123) went bump, PyPI, smoke test green and then failed at "Validate and publish server.json" with 422 ... "expected length <= 100". Commit 2d0b90a cut it to 96 characters on main at 18:04:59Z and the registry workflow was hand-dispatched: the first dispatch at 18:11:28Z failed, a second at 18:12:49Z succeeded. The registry's 0.12.2 entry therefore carries the text from main, not the text at the tag.

That was the second time the same wall was hit. At v0.11.0 the committed description was 105 characters; release run 35441067286 on 2026-09-19 went bump, PyPI, smoke green and failed the registry step with the same 422 at 11:47Z. e35ad7d cut it to 97, a manual dispatch succeeded at 11:52:41Z, and the registry's 0.11.0 publishedAt of 11:52:50Z is that dispatch. The version drift closed when a2a5c32 added server.json to .cz.toml version_files, with a comment reading "drifted to 0.4.0 once because it was missing from this list". "Once" undersells eleven tags, but the fix is right. v0.12.3 is the first tag whose committed server.json matches its version.

Stage two, mcpb/manifest.json. The same a465825 fixed the backend_type description and keywords; sentry had silently dropped out of the keyword list.

Stage three, pyproject.toml keywords and GitHub topics. At v0.12.2, keywords ended at aws-xray and never contained sentry. mcprush's auto-import reads pypi.org's JSON, which is how it surfaced. Commit 614f1a6, at 20:20Z on 2026-10-09 (01:50 IST on the 10th), added sentry, new-relic and honeycomb; they reached PyPI with the 11:12Z upload. GitHub topics were fixed with gh repo edit --add-topic and stand at 18 today.

The guard is tests/test_discoverability_sync.py: eight tests keyed off config.py's Literal. server.json choices must equal the Literal; the description must name every backend and stay at or under 100 characters, with a docstring recording that the limit has broken a live release twice; the MCPB description, MCPB keywords and pyproject keywords must cover every backend; the MCPB env mapping must cover every backend-specific variable. It passed on HEAD today. Two things it cannot reach because they are not file-backed are listed as manual steps in CLAUDE.md:261-280: GitHub topics and mcprush's "What it does" paragraph.

tracehub-mcp: four broken container images, and the CI that never ran one

v0.12.1 (b1f6722) describes itself as resolving 46 findings; no audit artifact exists outside the commit message, so that is its self-reported tally. One Critical: cache.py treated asyncio.CancelledError as an Exception, which it is not, so a cancelled leader never ran follower cleanup and every coalesced follower hung until restart.

Buried in that commit and never named in the CHANGELOG is the broken-container episode. Dependabot bumped the Dockerfile runtime stage to python:3.14-slim-trixie on 2026-09-17 (c7cf8d0, first shipped in v0.9.0) while the builder stayed on uv:0.12.15-python3.13-trixie-slim, so the runtime could not import what the builder installed. I checked this from each tag's committed Dockerfile rather than by pulling images: v0.9.0, v0.10.0, v0.11.0 and v0.12.0 all carry the mismatch, and Dockerfile:5-20 itself records the damage as "across 4 published releases". Both stages are now digest-pinned (Dockerfile:34, :68), tests/test_dockerfile.py pins both digests and asserts the runtime is python:3.13-slim-trixie (:39, :52, :66, :72), and ci.yml:128-247 runs a real container smoke test. The same release added a Trivy fail gate (ci.yml:336-357, if: always() so it fails closed).

Live posture at 89659c7: Scorecard 7/10, nine checks at 10, Code-Review 0 (solo maintainer), Maintained 0 (repo under the 90-day window). All 50 third-party uses: across nine workflow files are SHA-pinned. PyPI publishes via OIDC Trusted Publishing with PEP 740 provenance for both 0.12.3 files; the GHCR :0.12.3 index carries SPDX SBOM and SLSA provenance attestations.

A white padlock on an orange shield with its shackle lifting open and snapping shut

tracehub-mcp: the release pipeline, and the gate that has never run

The v0.12.3 run of release.yml (38047556471) went 4 of 4 green first try: bump 11:11:59 to 11:12:24, PyPI 11:12:26 to 11:13:01, install smoke test to 11:13:23, registry to 11:14:15. About 2 m 16 s end to end, with the registry entry published 1 m 54 s after the PyPI upload. Every version site at the tag reads 0.12.3 except uv.lock's own [[package]] block, still 0.12.2 and fixed by hand afterwards.

That is why there is one more fix on main that is in no release. Commit 89659c7, at 12:39Z, 87 minutes after the release, adds pre_bump_hooks = ["python3 scripts/sync_uv_lock_version.py"] to .cz.toml, a 76-line stdlib-only script that refuses unless exactly one [[package]] block matches. It also drops changelog_increment_filename from release.yml, which captured all of Commitizen's stdout and, per the comment at release.yml:57-63, put a [main <sha>] bump: ... trailer in "every GitHub Release body from v0.2.0 to v0.12.3"; all 19 bodies were edited by hand, and a regex scan over gh api output today matches the trailer in 0 of 19. tests/test_version_sync.py (129 lines, six tests) pins both, with a docstring reading "Two releases in a row (0.12.2, 0.12.3) shipped with uv.lock one version behind". The pipeline change has not been exercised by a release. If the hook fails, the bump aborts before anything is pushed or published, which is the right failure mode, but I am describing a test, not a run.

cost-guard-mcp: a cap off by up to 1536x, then fixing what the package says about itself

cost-guard-mcp's span splits cleanly in two. v0.2.0 and v0.3.0 made the number the server reports honest. v0.3.1 through v0.3.3 were mostly about making the package honest about itself, and the coda is that its discoverability surfaces are now the stale part. HEAD 84ec45a is the v0.3.3 release commit 151a172 plus one CI-only commit (#79); src/ is byte-identical to the tag.

cost-guard-mcp: the cap that was always checked against the smallest warehouse

This is the defect behind the "three orders of magnitude" claim, rated HIGH in v0.2.0 (PR #27). At v0.1.1, server.py had zero occurrences of warehouse_size. The pricing module knew about sizes, pricing/snowflake_pricing.py:30 has XSMALL at 1.0 credit/hour and :39 has X6LARGE at 512.0, but no tool parameter let a caller say which size they were on. Every Snowflake estimate was priced as XSMALL, so run_query_bounded(max_estimated_cost_usd=5) on an X6LARGE query compared a 512x-understated figure against the cap. The comment at tools/run_query_bounded.py:23-27 says it outright: the cap "would always be checked against the smallest warehouse's rate."

The fix threads warehouse_size and edition from server.py:117-123 and :136-145 through tools/estimate_query_cost.py:17-28 and tools/run_query_bounded.py:28. Two corrections to the changelog's own wording. "~512x" is exactly 512.0x (pinned by test_snowflake_pricing.py:12), and the figure belongs to Snowflake; the Databricks worst case is 173.7x. And the changelog mentions only size, but edition multiplies on top: snowflake_pricing.py:75-80 has standard $2.00, enterprise $3.00, business_critical $4.00, vps $6.00 per credit, so X6LARGE on VPS versus the XSMALL/standard default is a 1536x understatement. For Databricks, edition is silently ignored and warehouse is a documented no-op (engines/databricks.py:61-63) because the SQL warehouse is fixed by DATABRICKS_HTTP_PATH.

v0.2.0 also replaced the assumption that every query runs for 30 seconds. pricing/runtime_scaling.py (PR #29) has _TIERS at :16-21, ((10 GiB, 1), (100 GiB, 4), (1 TiB, 20), (inf, 100)), multiplying the 30-second baseline and never returning below it; the docstring admits the tiers are not telemetry-derived and "biases toward OVER-estimating ... never under." Snowflake QUERY_HISTORY calibration (PR #39, snowflake.py:105-163) replaces the heuristic entirely when the same query text has successful prior runs with bytes_scanned > 0. Databricks cannot be calibrated this way: its Query History API returns "<REDACTED>" for query text (README:196-201).

A cartoon bouncer in a black jacket and sunglasses standing with arms folded and legs spread wide over a small black square

cost-guard-mcp: a password that only lost its first word

The one CRITICAL in v0.2.0 (same PR #27, commit 0eaff07) is in errors.py. The v0.1.1 redaction regexes used a value class of [^\s,"\'}]+, which stops at whitespace, so password=hunter two redacted hunter and logged two. The fix at errors.py:25-28 introduces _BARE_WORD and _VALUE_FRAGMENT: a quoted value is consumed to its closing quote, and an unquoted value may contain spaces via a lookahead (?!{_BARE_WORD}\s*[=:]) that stops only at the next key. I ran both examples: password=my secret pass word; user=bob becomes password=***REDACTED***; user=bob; token: abc def ghi, host=foo becomes token=***REDACTED***, host=foo. One residual edge: the URI pattern at :47 stops at whitespace, so a malformed snowflake://user:p4ss w0rd@host would leak the password; no connector formats errors that way. The commit body records that the bug was "Flagged critical (3/3 adversarially confirmed) by a full-repo audit workflow", not by a human reading the regex.

A hand pressing a black patch over a hole in a clear water tank while water sprays out from under it

cost-guard-mcp: the third engine, labelled HEURISTIC for a reason

engines/databricks.py arrived in v0.2.0 (PR #26). It runs EXPLAIN COST {sql} (:131), parses sizeInBytes= out of the plain-text plan with the regex at :100, and always returns AccuracyTier.HEURISTIC (:169), because there is no dry-run and statistics are often absent. _parse_max_size_in_bytes at :108-113 returns int(max(sizes)) across plan nodes, not the sum, so a multi-table scan is represented by its single largest relation.

The semantic I most want users to understand: when no statistics parse, estimated_bytes is None but estimated_cost_usd is still a number, because :138-141 passes None to scale_runtime_hours, which returns the baseline. On X-Small Serverless that is 30/3600 h x 6 DBU/h x $0.70/DBU = $0.035. So run_query_bounded(max_estimated_cost_usd=N) passes a query of unknown size whenever N exceeds $0.035, while max_bytes_billed on the same query refuses (tools/run_query_bounded.py:67-82). A dollar cap alone on Databricks is a floor, not an estimate. Use the byte cap. The 5X-Large figure of 1042 DBU/h is from a Databricks-employee community blog about a Public Preview size, not the GA price list (pricing/databricks_pricing.py:9-23), and there is no live Databricks integration job.

The fourth tool, check_credentials (PR #21), runs no data-scanning query on any engine: BigQuery calls get_service_account_email(), Snowflake SELECT CURRENT_ROLE(), CURRENT_WAREHOUSE(), CURRENT_ACCOUNT(), Databricks SELECT current_user(); failures return ok=False with a redacted detail. v0.2.0 also brought Snowflake and the new Databricks engine in line with what BigQuery already did at v0.1.1, stripping a trailing ; before wrapping under the row cap; at v0.1.1, SELECT 1; broke the Snowflake wrap.

cost-guard-mcp: Gen2 warehouses, a cancel that did nothing, and signing the artifact

Gen2 detection (v0.3.0, PR #47). Gen2 warehouses cost 1.35x Gen1 on AWS and GCP, 1.25x on Azure (snowflake_pricing.py:46-71). snowflake.py:166-228 runs SHOW WAREHOUSES LIKE '<validated>', reads "generation" via RESULT_SCAN, then SELECT CURRENT_REGION(), each with a 5-second timeout; without warehouse, or on any failure, it prices as Gen1 and appends a caveat. The repo contradicts itself here: snowflake.py:186-189 says no live Snowflake account was available to verify; changelog/v0.3.0.md:14-16 says it was live-verified against COMPUTE_WH. The changelog is right, the docstring is stale. The weekly integration test runs detection live but asserts only tier == UPPER_BOUND and non-None bytes and cost; it would pass identically if detection fell back to Gen1. Live-exercised, not live-asserted.

MCP cancellation was inert (PR #49, e292934). At v0.2.0 run_query_bounded was a plain def (server.py:74 at that tag); the SDK runs sync tools with abandon_on_cancel=False, so a host's cancellation notification waited for the thread anyway. v0.3.0 made the tool async def over anyio.to_thread.run_sync(..., abandon_on_cancel=True) (server.py:58-89, :88). test_server_cancellation.py:78-108 runs a real server over stdio with BigQuery mocked to block 8 seconds, cancels at 1 second, and asserts a return under 4. What still does not cancel: the abandoned thread and the warehouse-side query run until the 120-second watchdog.

Signing (PR #45) rewrote release.yml. The build job gates on tag == pyproject.toml == server.json version (:28-36) and exports a CycloneDX 1.5 SBOM; the publish job runs astral-sh/attest-action (:64) then uv publish under Trusted Publishing; the release job runs actions/attest-build-provenance (:84), renames the bundle provenance.intoto.jsonl for Scorecard's Signed-Releases check, and attaches wheel, sdist, SBOM and provenance. Live proof for 0.3.3: PyPI's /integrity/cost-guard-mcp/0.3.3/<file>/provenance returns 200 for wheel and sdist, and gh attestation verify cost_guard_mcp-0.3.3-py3-none-any.whl --repo mcpsmiths/cost-guard-mcp exits 0 with the build signer release.yml@refs/tags/v0.3.3. Scorecard for 84ec45a is 7.4/10, with an eight on Signed-Releases.

cost-guard-mcp: twenty-two minutes and zero lines of code

v0.3.1 (#59) added four [project.urls] entries to pyproject.toml because mcp-marketplace.io's package-verification scanner flagged that the PyPI package had no GitHub URL. v0.3.2 (#62) landed twenty-two minutes later and added two trove classifiers, Programming Language :: Python :: 3 and :: 3.12 (pyproject.toml:8-15), because the README's shields.io pypi/pyversions badge rendered "missing"; it reads classifiers, not requires-python. Neither changed a byte of src/ (git diff v0.3.0 v0.3.2 -- src/ is empty). Same failure class as tracehub-mcp's keywords, surfaced by a different external reader.

cost-guard-mcp: an empty string, a static reader, and seventeen days

Empty passphrase (#63, 0669c8a). GitHub Actions renders a missing secret as "", and integration.yml:48 passes SNOWFLAKE_PRIVATE_KEY_PASSPHRASE unconditionally. The connector received an empty passphrase for an unencrypted key and failed with Password was given but private key is not encrypted. The fix at config.py:73-80 is os.environ.get("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE") or None. It merged 2026-09-23 and released today. Seventeen days fixed-but-unreleased is the number I am least proud of in this span.

Docker base re-pin (#76). Dockerfile:12 now pins ghcr.io/astral-sh/uv:python3.12-trixie-slim@sha256:17af15d1... for pcre2 CVE-2026-103111 and openssl CVE-2026-84782. I pulled that digest, rebuilt, and ran Trivy 0.74.0 with CI's exact flags: exit 0 against today's database. oauthlib accepted (#77, #79): databricks-sql-connector pins oauthlib<4.0.0, which pins out fixes to authorization-server-side issues; this project is only an OAuth client. audit.yml:26-37 records "Accepted 2026-10-10", "Re-check by 2026-12-01".

Single-line @mcp.tool (#73, 8a4eac4), a change made purely for a third-party reader. mcprush.com's "Read the tools again" static scanner reported "found no tool declarations" against decorators that were multi-line @mcp.tool(annotations=ToolAnnotations(...)) stacked over @as_tool_error. The restructure moves annotations into _*_ANNOTATIONS constants (server.py:38-57) and puts a single @mcp.tool(...) directly above each def. I round-tripped the published 0.3.3 wheel with a real stdio ClientSession: initialize succeeds on protocol 2025-11-25, tools/list returns four tools with readOnlyHint false only on run_query_bounded, and describe_engine_capabilities(databricks) returns HEURISTIC. Doing that, I noticed server.py:31 constructs MCPServer("cost-guard-mcp") with no version, so server_info.version is an empty string on the wire. A client cannot tell 0.3.2 from 0.3.3 at initialize, which, given what the directories showed for most of today, is the one place it would help.

cost-guard-mcp: the tool that spends money is the one that never logs

v0.3.0 (#50) added tool-outcome logging: errors.py:83-91 _log_tool_outcome emits one INFO record, tool=%s outcome=%s elapsed_ms=%.1f. README:155-157 says every tool call logs. That is false for exactly one tool. The sync wrapper at errors.py:133-156 logs on every exit path; the async wrapper at :119-131 has no _log_tool_outcome call. run_query_bounded is the only async tool, made async by #49 the same day #50 landed logging on the sync branch. I confirmed it empirically: run_query_bounded logs nothing at the tool layer, including on a successful execution, and test_errors.py:204-255 only exercise a sync def some_tool. What it does log is refusals. SQL text is never logged anywhere in src/.

OpenTelemetry: the gate is right, the packaging is broken. server.py:173-198 returns early when OTEL_EXPORTER_OTLP_ENDPOINT is unset, otherwise imports the SDK and gRPC exporter inside the function (:190-194). README:179-183 says this "Requires the otel extra: uv sync --extra otel / pip install cost-guard-mcp[otel]". v0.3.0 moved otel to a PEP 735 [dependency-groups] entry (pyproject.toml:49-52), which is not published in wheel metadata; PyPI 0.3.3 has provides_extra: None. Live: uv sync --extra otel errors with Extra 'otel' is not defined, and uvx --from "cost-guard-mcp[otel]==0.3.3" python -c "import opentelemetry.sdk" raises ModuleNotFoundError. Worse, set the env var without the group installed and :190-194 has no try/except, so the server does not start. For PyPI users the only route is uvx --with opentelemetry-sdk --with opentelemetry-exporter-otlp-proto-grpc cost-guard-mcp, which no repo doc mentions.

Two servers, one failure class, two distances from the fix

Set the two lists side by side and the shape is the same. In both repos the executable artifact was right; what drifted was what each package said about itself to a reader that was not the Python interpreter. tracehub-mcp's server.json said 0.4.0 for eleven tags while the registry showed the right number, because a jq rewrite papered over the gap at publish time; cost-guard-mcp's server.json says 0.3.3 today and the registry shows 0.3.2, because nothing ships it at all. tracehub-mcp's keywords missed sentry and mcprush imported the gap; cost-guard-mcp has no keywords field for mcprush to import. tracehub-mcp's README says "all five backends" with eight shipped; cost-guard-mcp's README says --extra otel for an extra that does not exist. Both mcprush listings sat a version behind for the whole gap, tracehub's at 0.12.2 since 20 September and cost-guard's at 0.3.2 since its 6 October scan, and both moved to today's versions only after a manual "Read the tools again" from the publisher side this afternoon. mcprush re-reads when the publisher asks, not when PyPI changes, so a listing stays wherever it was last pointed.

Where the two diverge is what happened next. tracehub-mcp's quiet twenty days produced test_discoverability_sync.py, server.json in Commitizen's version_files, test_dockerfile.py plus a CI container smoke test, test_version_sync.py plus the pre-bump hook, and cz changelog --dry-run for the release bodies. Every drift defect in its list has a gate behind it, and exactly one has never been exercised by a release. cost-guard-mcp's seventeen days between the passphrase fix merging and today's release were not quiet, #63 and #73 through #79 all landed, and they produced the fixes that make the server safer to run and the signing that makes the artifact verifiable, which is the right priority order. But its release.yml still has no registry step, its pyproject.toml still has no keywords, and the fix for its stale registry entry is a manual mcp-publisher publish on my machine. The durable version already exists: tracehub-mcp/.github/workflows/release.yml:186-196 calls a reusable OIDC mcp-registry-publish.yml, and tracehub's registry entry is 0.12.3 today, published 1 m 54 s after its wheel. Porting it is the first item on cost-guard-mcp's list.

tracehub-mcp caught its drift because it has more surfaces: a published GHCR image, an MCPB manifest, a registry step in the pipeline. Each surface is a reader that can disagree with the repo, and each disagreement is a test you did not know you needed. Fewer surfaces is not the same as fewer gaps.

Where to get them, and how to use each from every surface

Everything in both tables was checked live today, 2026-10-10; the mcprush rows were re-checked at about 16:20Z, after the listings re-read.

tracehub-mcp v0.12.3

Surface What is live today How you use it
PyPI tracehub-mcp Current. 0.12.3, wheel uploaded 11:12:53Z, sdist 11:12:55Z; 19 releases 0.2.0 through 0.12.3; requires_python >=3.11; 16 keywords now including sentry, aws-xray, new-relic, honeycomb; 2,158 downloads in the last 30 days per pypistats; PEP 740 provenance served for wheel and sdist. uvx tracehub-mcp --backend jaeger --url http://localhost:16686, or pipx install tracehub-mcp / pip install tracehub-mcp then tracehub-mcp --backend .... HTTP: uvx tracehub-mcp --transport http --host 0.0.0.0 --port 8000, clients connect to http://<host>:8000/mcp.
Official MCP Registry io.github.mcpsmiths/tracehub-mcp Current. 15 versions, all active; 0.12.3 isLatest: true, published 11:14:12Z; 96-char description naming all eight backends; one pypi package, 18 env vars, BACKEND_TYPE with 8 choices. The 0.12.0 and 0.12.1 entries are immutable and still describe six backends. ?search=tracehub surfaced 0.10.0 first today; use GET /v0.1/servers/io.github.mcpsmiths%2Ftracehub-mcp/versions. Metadata only. Registry-aware clients read the pypi entry and run uvx tracehub-mcp with the declared env vars.
mcprush mcprush.com/mcpsmiths/tracehub-mcp Current as of this afternoon: "v0.12.3 · 10 Oct 2026", "Release 0.12.3 · scanned 10 Oct 2026". It read "v0.12.2 · 20 Sep 2026" from the September burst until a manual "Read the tools again" today; mcprush does not re-read on its own. Grade A, trust 99/100, 19 tools listed. Still prints "Publisher not claimed" although the mcpsmiths account owns the listing; support ticket pending. The -1 for "no provenance" is wrong given PEP 740. Every tool shows "No parameter schema on file" because the tools were statically extracted from source, not enumerated from a running server. Their quickstart, verbatim: claude mcp add --scope user --env 'BACKEND_TYPE=${BACKEND_TYPE}' --env 'BACKEND_URL=${BACKEND_URL}' --transport stdio tracehub-mcp '--' uvx tracehub-mcp
GHCR ghcr.io/mcpsmiths/tracehub-mcp Current. :0.12.3 and :latest both pull publicly, same index digest sha256:bda6e443..., linux/amd64 + linux/arm64 plus SBOM and SLSA attestations; non-root mcpuser (uid 1000); HEALTHCHECK hits /ready. 17 tags: 14 version tags 0.4.0 through 0.12.3, latest, and two manual-<run-id> tags. Tags 0.9.0 through 0.12.0 were built from a 3.13-builder / 3.14-runtime Dockerfile and will fail at import; verified from the tagged Dockerfiles, not by pulling. docker run --rm -p 8000:8000 -e BACKEND_TYPE=jaeger -e BACKEND_URL=http://host.docker.internal:16686 ghcr.io/mcpsmiths/tracehub-mcp:0.12.3, then point clients at http://localhost:8000/mcp. Disable the HEALTHCHECK for stdio.
MCPB bundle (Claude Desktop Extension) Source only, published nowhere. mcpb/manifest.json is at 0.12.3 with server.type: "uv" and user_config for every backend. No .mcpb is attached to any of the 19 GitHub Releases (0 assets each). mcpb pack mcpb/ tracehub-mcp.mcpb (the command .gitignore:12-14 anticipates), then import into Claude Desktop. Requires uv on the machine.
GitHub mcpsmiths/tracehub-mcp Current. 19 Releases, latest v0.12.3 created 11:12:18Z by github-actions[bot], 0 assets each; 18 topics including all eight backends; 0 stars, 0 forks, 0 open issues; 9 open code-scanning alerts, all Scorecard, 0 Dependabot. gh release view v0.12.3 -R mcpsmiths/tracehub-mcp
Glama, M8ven Both carry a listing, linked via badges at README.md:10-11. I did not audit either listing's metadata today; treat whatever they show as unverified. Follow the badge links; both resolve to the same uvx tracehub-mcp install.
Smithery No listing. Both slug variants return a 200 SPA shell whose body reads "Not Found", the same as a nonsense slug; the search API returns zero servers for tracehub or mcpsmiths. Nothing to use.

The Claude Code one-liner from README.md:71:

claude mcp add tracehub-mcp -e BACKEND_TYPE=jaeger -e BACKEND_URL=http://localhost:16686 -- uvx tracehub-mcp
Enter fullscreen mode Exit fullscreen mode

Per backend you add what its __init__ demands: Datadog -e BACKEND_API_KEY=... -e BACKEND_APP_KEY=...; Sentry BACKEND_API_KEY plus BACKEND_SENTRY_ORG; X-Ray BACKEND_AWS_REGION with credentials from the boto3 chain; New Relic a User key in BACKEND_API_KEY plus BACKEND_NEWRELIC_ACCOUNT_ID; Honeycomb a Configuration key plus BACKEND_HONEYCOMB_DATASET.

Claude Desktop, at ~/Library/Application Support/Claude/claude_desktop_config.json or %APPDATA%\Claude\claude_desktop_config.json:

{
  "mcpServers": {
    "tracehub-mcp": {
      "command": "uvx",
      "args": ["tracehub-mcp"],
      "env": { "BACKEND_TYPE": "jaeger", "BACKEND_URL": "http://localhost:16686" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cursor (.cursor/mcp.json), Windsurf (~/.codeium/windsurf/mcp_config.json) and Gemini CLI (~/.gemini/config.json) use the same mcpServers shape. VS Code with GitHub Copilot uses .vscode/mcp.json with the top-level key servers, not mcpServers, and no type field for stdio (README.md:557-574). Codex CLI is not among the six clients tracehub-mcp's README documents; the stdio command, args and env are identical and you would register them per Codex's own MCP docs.

cost-guard-mcp v0.3.3

Surface What is live today How you use it
PyPI cost-guard-mcp Current. 0.3.3 uploaded 11:01:04Z; 7 releases; project_urls present; classifiers Python 3 and 3.12 only; 777 downloads/30d; PEP 740 attestations under /integrity/. Gaps: keywords: None (pyproject has no keywords field); provides_extra: None so [otel] installs nothing; server_info.version empty on the wire. uvx cost-guard-mcp · pip install cost-guard-mcp · pinned: uvx cost-guard-mcp==0.3.3
GitHub Release mcpsmiths/cost-guard-mcp v0.3.3 Current, "Latest", 11:01:17Z. Assets: wheel (46,521 B), sdist (257,326 B), provenance.intoto.jsonl (SLSA), sbom.json (CycloneDX 1.5); tracehub-mcp's releases, by contrast, carry 0 assets. Download the wheel, then gh attestation verify cost_guard_mcp-0.3.3-py3-none-any.whl --repo mcpsmiths/cost-guard-mcp.
Official MCP Registry io.github.mcpsmiths/cost-guard-mcp Stale: isLatest is 0.3.2, published 2026-09-28T11:38Z, nearly six days after its tag. Registry has 0.1.0, 0.1.1, 0.2.0, 0.3.0, 0.3.2; 0.3.1 and 0.3.3 were never published. release.yml has no registry step; every publish has been manual. server.json in the repo says 0.3.3 but nothing ships it. Search via the README:7 badge. Manual fix on my side: mcp-publisher login github -token <PAT> && mcp-publisher publish. Durable fix: port tracehub-mcp's reusable OIDC mcp-registry-publish.yml.
mcp-marketplace.io Mirrors the registry, so it shows 0.3.2. This is the scanner that triggered v0.3.1. Its risk score and install counter render only in a browser, and I have not re-verified them today. Follows the registry automatically once it is re-published.
mcprush mcprush.com/mcpsmiths/cost-guard-mcp Current as of this afternoon: "v0.3.3 · 10 Oct 2026", "Release 0.3.3 · scanned 10 Oct 2026". Until I clicked "Read the tools again" in Studio at about 15:20 IST it showed 0.3.2 under an "Always latest" label, a Security tab dated 6 Oct 2026, and the literal line "No tools were enumerated" from a scan that predated #73. The re-scan now reports "Tools were statically extracted from the published source (4 recovered)", 3 threat findings and 0 capability findings, a capability-exposure −6 for the one write tool, and the same wrong −1 "no provenance" (PEP 740 attestations exist). Grade A, trust 93/100, Publisher Verified, 157 downloads/30d. Each of the 4 tools still shows "No parameter schema on file". Their quickstart: claude mcp add --scope user cost-guard-mcp '--' uvx cost-guard-mcp. Refresh is publisher-only via Studio.
Docker / GHCR No published image. Anonymous ghcr.io token request for mcpsmiths/cost-guard-mcp returns 403 (control: mcpsmiths/tracehub-mcp returns 200 with 17 tags). The image never installs the OTel group; OTEL_EXPORTER_OTLP_ENDPOINT crashes startup. Local build: docker build -t cost-guard-mcp . then docker run -i --rm -e GOOGLE_APPLICATION_CREDENTIALS=/creds.json -v /path/to/sa.json:/creds.json cost-guard-mcp (README:67-72).
MCPB bundle None. No manifest.json or .mcpb in the repo. Nothing to use.
OpenTelemetry opt-in Gate works; packaging broken. README's --extra otel / [otel] do not work. Source: uv sync --group otel. PyPI: uvx --with opentelemetry-sdk --with opentelemetry-exporter-otlp-proto-grpc cost-guard-mcp.
GitHub repo metadata 11 topics including bigquery and snowflake; no databricks topic, 23 days after the engine shipped. 1 star, 0 forks. AGENTS.md:7-9 still says "three tools ... BigQuery and Snowflake". gh repo edit mcpsmiths/cost-guard-mcp --add-topic databricks
Source Current. git clone https://github.com/mcpsmiths/cost-guard-mcp && cd cost-guard-mcp && uv sync && uv run cost-guard-mcp; dev: uv sync --all-groups.

The Claude Code one-liner. The bare form sets no credentials, so every engine's check_credentials returns ok: false until you add -e flags; the host spawns a subprocess and your shell env is not inherited (README:46-48):

claude mcp add --scope user cost-guard-mcp -- uvx cost-guard-mcp
Enter fullscreen mode Exit fullscreen mode

BigQuery: insert -e GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json before --. Snowflake: -e SNOWFLAKE_ACCOUNT=... -e SNOWFLAKE_USER=... -e SNOWFLAKE_PRIVATE_KEY_PATH=... (or -e SNOWFLAKE_PASSWORD=...). Databricks: -e DATABRICKS_SERVER_HOSTNAME=... -e DATABRICKS_HTTP_PATH=... -e DATABRICKS_TOKEN=.... Or claude mcp add-json cost-guard-mcp '<object from .mcp.json.example>'.

Claude Code .mcp.json and Claude Desktop: drop .mcp.json.example:1-19 (command: uvx, args: ["cost-guard-mcp"], env block) into .mcp.json or claude_desktop_config.json as-is, with one gap: .mcp.json.example:8 and README:139 ship a BIGQUERY_PROJECT key that no server code reads; server.json's 12 env vars match config.py's 12 reads exactly, and the example carries a thirteenth dead key. Cursor: .cursor/mcp.json or ~/.cursor/mcp.json, same object plus "type": "stdio" (README:126). VS Code with GitHub Copilot: .vscode/mcp.json, rename mcpServers to servers, add "type": "stdio" (README:127). Codex CLI, which cost-guard-mcp's README does document: codex mcp add cost-guard-mcp -- uvx cost-guard-mcp, or the [mcp_servers.cost-guard-mcp] TOML block at README:132-147.

If you discover cost-guard-mcp through the official registry or mcp-marketplace.io, you are shown 0.3.2. That is a publishing gap on my side, not a different package: neither page pins a version, so its install command resolves to 0.3.3 from PyPI anyway. mcprush showed the same stale version for both servers until this afternoon, and would still if nobody had clicked.

Three analog clocks labelled New York, Berlin and Sydney showing three different times while their red second hands sweep

What is still stale today

tracehub-mcp. README.md:97 says "all five backends"; README.md:105 and :939 say "458 passing tests (2 skipped)" against 1173 today; the CLI flag list at README.md:202 omits --newrelic-account-id, --honeycomb-dataset and --query-cache-ttl-seconds. SECURITY.md:23, :52, PRIVACY.md:26 and base.py:159 all say five backends. PRIVACY.md:28 claims no other outbound network calls, omitting FastMCP's PyPI update check every 12 hours (opt out with FASTMCP_CHECK_FOR_UPDATES=off) and X-Ray's boto3 traffic. sentry.py:14-20 still says no live account. start_locally.sh still knows only Jaeger, Tempo and Traceloop. None of this is reachable by test_discoverability_sync.py, because none of it is the metadata that file guards. It is prose, and prose drifts.

cost-guard-mcp. The official MCP Registry, and mcp-marketplace.io by mirroring it, show 0.3.2. PyPI has no keywords. The README advertises an otel extra that does not exist (README:179-183, echoed at server.py:183). run_query_bounded, the one tool that spends money, never logs its outcome. server_info.version is empty at initialize. .mcp.json.example:8 carries a dead BIGQUERY_PROJECT. There is no databricks topic. snowflake.py:68-72 still names history correlation the "highest-value follow-up", which shipped in the same release; snowflake.py:186-189 still says Gen2 detection was never live-verified; tools/describe_engine_capabilities.py:33-36, :56-59 still say the dollar figure "assumes a fixed placeholder runtime", false since v0.2.0. AGENTS.md:7-9, ARCHITECTURE.md:14-66 and SECURITY.md:30-48 still describe three tools and two engines. Dated obligations: oauthlib re-check by 2026-12-01; the 21 Trivy ignores expire 2026-11-15; Databricks 5X-Large pricing is pending GA.

The difference between the two lists is not length. tracehub-mcp's list is prose. cost-guard-mcp's list includes the registry entry, the keywords and the extra, which are exactly the things tracehub-mcp now has a test for.

What this means if you are upgrading today

tracehub-mcp. If you run GHCR tags 0.9.0 through 0.12.0, your image was built from a Dockerfile whose builder and runtime disagree on the Python minor version and will fail at import; move to :0.12.3 or :latest, the same digest. The Docker HEALTHCHECK was liveness-only until v0.12.1 and is readiness now, so an orchestrator that saw healthy during a backend outage will now see 503. If you configured the server from the official MCP Registry at 0.12.0 or 0.12.1, you could not see New Relic or Honeycomb; the 0.12.3 entry has all eight. If you were on Datadog or Sentry before v0.6.0, every gen_ai-dependent tool was silently returning empty results against a real account; that is fixed and verified live. If you are adopting X-Ray, New Relic or Honeycomb, read the module docstring before you trust the output, and know that Honeycomb will report healthy on a Free or Pro plan and then refuse every query with HoneycombEnterpriseRequiredError. If you consume outputSchema, six tools have real ones and thirteen still return a wrapped string.

cost-guard-mcp. If you installed via uvx cost-guard-mcp or pip, you get 0.3.3 on next resolution; if you came in at v0.1.1, upgrade without deliberating. Every Snowflake dollar figure before v0.2.0 was priced as XSMALL/standard/Gen1 running for 30 seconds; pass warehouse_size, edition and warehouse now, or the caveats will tell you the number is a floor. Multi-word secrets in connector errors were partially logged before 0.2.0; they are not now. Host-side cancellation returns within seconds since 0.3.0, though the warehouse-side query runs on until the 120-second watchdog. On Databricks, max_estimated_cost_usd on a query with no parseable statistics compares your cap against a $0.035 floor and passes; set max_bytes_billed and let it refuse on None. If SNOWFLAKE_PRIVATE_KEY_PASSPHRASE comes from a secret store that may render it as an empty string, 0.3.3 is the first release that treats that as "no passphrase". If you set OTEL_EXPORTER_OTLP_ENDPOINT against a PyPI install or the Docker image without also installing the two opentelemetry packages, the server will not start.

Both. Verify what you installed from the index, not from the server. For tracehub-mcp, PyPI's PEP 740 provenance and the GHCR SLSA attestation name release.yml in mcpsmiths/tracehub-mcp as the builder. For cost-guard-mcp, PyPI's /integrity/ endpoint and the GitHub Release's provenance.intoto.jsonl do the same for mcpsmiths/cost-guard-mcp, which is more than the server itself will tell you at initialize until I pass it a version. And treat the directories as what they are: the official registry is a version behind on one server, and mcprush caught up on both only because someone clicked a button today. The next tag of tracehub-mcp will tell me whether the uv.lock pre-bump hook holds. The next tag of cost-guard-mcp should be the first one that reaches the registry without my hands on the keyboard, and if it is not, the fix is already written in the other repo.

Top comments (0)