Repository navigation
Conversation
- add package setup and scripts - add strict typescript config - add empty app entry point
- add the official mcp typescript sdk - add zod for tool schemas - lock dependency versions
- add the deeptrace server identity - keep server setup reusable for transports and tests
- add type aware eslint rules - add prettier formatting - add lint and format scripts
- install dependencies from the lockfile - check formatting lint types and build - run on pushes and pull requests
setup mcp boilerplate
Feat/set up workflow
Server side setup and standardized output format
- add Vitest scripts and MCP server factory coverage - share TypeScript test configuration with source fixtures - run unit tests in the code quality workflow
* add MCP stdio lifecycle - connect the server through a reusable stdio runtime - handle clean idempotent signal shutdown - test initialization and lifecycle behavior - document local build and client setup * clarify Nuthatch deployment documentation
* add source adapter runtime validation - mirror the canonical source interface with strict Zod validators - enforce source-specific success and failure invariants - validate existing fixtures and malformed boundary cases * co-locate source schemas and types - make the canonical source schema the runtime and type source of truth - remove the duplicate runtime schema module - use concrete schema-derived result types for fixtures
M2.2: Base DEX candidates
* add shared gateway primitives - add typed configuration and rate-limit errors - add validated gateway defaults and environment overrides - add injected-clock fixed-window limiting with unit coverage * address gateway review findings - prune expired rate-limit keys on cleanup boundaries - keep rate-limit error text fixed and secret-safe - cover pre-boundary rejection and expired-key eviction
M2: Graph probe harness
* define M0 core policy * document M0 core policy * clarify M0 policy ownership
feat(m2): add Graph source validation pipeline
The page ships no script, so client configurations use native disclosure elements and code blocks select on click rather than through a copy button. Its three subset typefaces are served from this origin so the document depends on nothing external.
The inline HTML constant becomes a file read, so the page is editable as a document instead of a template literal. font-src moves from 'none' to 'self' to admit the three same-origin typefaces. script-src stays 'none' and style-src still omits 'self', so styles remain inline. The typeface table is exact-match: request paths are only ever compared against its keys, so no caller-supplied string reaches the filesystem and traversal is not expressible.
The typefaces are the only non-MCP paths this origin answers, so they resolve ahead of the MCP path check and before authentication. The build now copies .html and .woff2 beside the emitted JavaScript, since tsc emits only JS and the runtime reads these at startup. Prettier skips the page because its <pre> whitespace is rendered output, and reformatting the inline stylesheet tripled the file.
Asserts the typefaces are served unauthenticated with immutable caching, that unknown and traversal-shaped asset paths are refused, and that the page links to nothing beyond its own fonts.
M0.11: rebuild the public connection page
* feat(registry): pin Messari standardized deployments on Base Replace the native Uniswap V3 and PancakeSwap records with one Messari dex-amm deployment bound twice, once per WETH/USDC fee tier, and add the three Messari lending deployments behind a new "lending" category. The compare-lending profile mirrors the compare-pools join, so both profiles share the loader's duplicate, category, and same-tier checks rather than growing a second set of validation rules. * feat(graph): read pool metrics from the Messari dex-amm standard Dispatch the pool adapter on query_id so the Tier-A Messari path and the native Tier-B path can coexist, and extract the parsing helpers both share into response.ts. Tier-A differs from the native schema in three ways that are easy to miss: inputTokens replaces token0/token1 and reports decimals as an Int, the fee tier lives in a fees array as a percentage rather than a bps integer, and snapshots are keyed by day-since-epoch rather than a midnight timestamp. The adapter converts the last one before handing snapshots to the shared completed-UTC-day aggregator, so window semantics stay identical across both tiers. The standard publishes no per-day fee field, so fees_usd maps to dailyTotalRevenueUSD. That is supply-side plus protocol-side revenue; on both compared pools the protocol-side share is zero today, but that is a property of the pools rather than a conversion performed here. * feat(lending): define the compare_lending_markets contract Lock the USDC market, the five ranking metrics, and the three-source coverage expectation in policy, then add the request and response schemas and the scope binding they validate against. The market token is policy rather than a request field: the tool compares one asset across every selected protocol, so letting callers vary it would change what the comparison means. Rates are carried as decimal strings with an explicit percent_apy basis so no layer is tempted to convert between APR and APY. * feat(lending): query the three Base lending deployments One query document serves Aave v3, Seamless, and Moonwell unchanged, which is the whole reason for standardizing: the sources differ by deployment, not by adapter. Two decisions worth naming. The adapter reads lendingProtocols.network back and rejects anything that does not self-report BASE, because a deployment published for one network can index another -- the Compound v3 "base" deployment reports MAINNET, which is why it is out of scope. Second, an absent rate pair stays null and silent because Moonwell genuinely publishes no stable borrow rate, while a duplicated or malformed pair yields null plus a warning, since both defaulting to zero and picking arbitrarily would invent a number no source reported. * feat(lending): rank and settle lending market results Rank by the requested metric with nulls last and deterministic protocol/market/source tie-breaks, then settle into the same status/coverage/freshness/provenance envelope compare_pools returns. Each protocol contributes at most one market for the locked asset, so unlike compare_pools there is nothing to deduplicate across sources. A market reporting isActive false is still ranked and returned with the flag and a warning, because its balances and rates remain real. * feat(mcp): register compare_lending_markets Wire the tool behind an injected source gateway and advertise it beside compare_pools with the same read-only annotations. The live lending gateway defers registry loading into fetchLendingResults rather than resolving it at construction, so creating the server cannot throw on a registry problem. Tests inject both gateways to stay offline. * test(smoke): exercise both tools in the MCP smoke check Require every public tool in tools/list and call each one, so a tool that is built but never registered cannot pass unnoticed. * docs: describe the Messari scope and the lending tool Record the pinned deployments, the field mapping, and the two caveats a reader will otherwise trip over: fees_usd is revenue rather than a fee field, and Compound v3 is excluded for self-reporting MAINNET. The superseded native Tier-B selection is kept at the end of source-scope so the earlier sweep's findings are not lost. Extend the existing skill to cover both tools rather than adding a second one, since an agent choosing between them needs a single set of rules. * fix(quality): count sources that answered, not records returned Both tools derived successful coverage from the post-Top-N record count, so asking for top_n 1 while all sources were healthy and fresh reported one successful source and settled partial. Top-N is the caller's choice, not a coverage gap, and the truncation already has its own warning. Coverage now counts the sources that returned ok. The response schemas relax from requiring equality to requiring that records never outnumber the sources that answered, which still catches the inconsistency the equality check was there to prevent.
One shared deployment token means a single leak compromises every client with no way to revoke selectively. The store issues per-client tokens and keeps only their SHA-256 digests, so the file is enough to revoke a token but never to use one. Verification is a digest lookup rather than a comparison, so its cost does not grow with the number of issued tokens and no timing-safe compare is needed: a digest cannot be walked back to the secret. isAuthorized accepts issued tokens and the shared token together, so existing clients keep working while the shared one is retired.
Access is open, so the endpoint is unauthenticated: a caller cannot present a token before something has given them one. The page carries its own policy because the connection page forbids forms outright, and this one needs exactly a single form. The per-address limit is cost control rather than access control. Anyone may hold a token; nobody may mint an unbounded number of them, since each one spends metered Graph quota.
The page told visitors to request a token through a secure channel, which left no path for anyone who did not already know the maintainer. It now links to the issue endpoint.
Two properties of the deployment make the store path load-bearing and are easy to undo by accident: ProtectSystem=strict leaves the filesystem read-only, so the directory has to be one systemd made writable, and the deploy rsyncs /opt/deeptrace with --delete, so a store inside that tree would be discarded on every deploy.
The other cases post without an Origin header, so they never exercised the origin check that stands in front of the endpoint. That check also refuses a mint posted from another site, which is the CSRF case.
M9: self-serve per-client tokens
* feat(wallet): define research contracts and cursor scope Freeze the Base-only request, section-aware response, registry capabilities, and wallet-bound fixed-snapshot cursor before source implementations depend on them. * feat(wallet): verify Aave wallet positions Bind wallet research to the proven Aave deployment and preserve exact position balances, snapshot valuation, freshness, and evidence without repricing. * feat(wallet): index bounded pool activity Add a dedicated sender/recipient Nuthatch capability with safe bounded scans, parity checks, source-local failures, and live Graph/Nuthatch composition. * feat(wallet): expose research_wallet through MCP Register the source-bounded wallet tool beside compare_pools, compare_lending_markets, and find_large_swaps so clients can request supported Base wallet facts without implying complete balances. * docs(wallet): define the supported research boundary Document the verified Graph scope, pool-bounded Nuthatch limitations, client workflow, and remaining live deployment gate so partial results cannot be mistaken for complete wallet history.
Copying a token by hand is the friction. OAuth removes it: the client opens a page, the user approves, and the token returns to the client directly. The authorization server runs inside the resource server, which drops the parts that usually make OAuth expensive. There is no second host, no signing key and no JWKS, because a token can be looked up in the store rather than verified from a signature. PKCE with S256 is mandatory and callbacks must be loopback: a public client keeps no secret, so the redirect target is all that binds a code to its requester. An unrecognised callback renders an error rather than redirecting, since redirecting there is the open redirect it is meant to prevent.
Clients discover the browser flow from the challenge. Without resource_metadata the 401 is a dead end and the user copies a token by hand.
Follows one client from registration through consent to a token that authenticates MCP, then covers the failures that matter: a mismatched PKCE verifier, a replayed code, an unregistered callback, and a missing challenge.
M9.2: OAuth browser flow — no token copying
The allowlist ran ahead of routing, so any OAuth call carrying an Origin header was refused with invalid_origin. Clients set that header themselves — a CLI, an editor shell, a loopback callback — and those values cannot be enumerated, so the allowlist rejected working clients and nothing else. Discovery, registration and token exchange are now exempt; PKCE and single-use codes defend them. The MCP endpoint and the consent submission still require an allowed origin, which is where a cross-site POST is the actual threat. The earlier tests missed this because they sent no Origin header at all.
Fix: origin allowlist blocked every OAuth client
DEEPTRACE_HTTP_TOKEN authenticates every client at once, so one leak exposes all of them and no single client can be revoked. Issued per-client tokens already stand on their own, which leaves the shared credential necessary only until pre-migration clients move over. Omitting the variable now retires it instead of failing startup, making retirement an operational step rather than a code change. A value that is present but too short is still rejected, since a weak shared secret is worse than none, and startup warns while the variable remains set.
The page sent Referrer-Policy: no-referrer, which is the one policy that makes a browser set Origin to the literal string null. The origin allowlist that guards minting does not contain null, so every real click on Create a token answered 403 invalid_origin. Only curl succeeded, because a request with no Origin header at all is treated as same-origin. same-origin keeps the referrer off other sites while leaving the page's own form carrying a real origin, so the allowlist still refuses a cross-site post. Existing tests passed throughout: they send either the allowed origin or an attacker's, never the value a browser actually sends from this page. The added test pins the header and the null-origin rejection together, so reintroducing no-referrer fails in CI rather than in the browser.
* docs(readme): rewrite for a first-time reader The opening was a single dense sentence that only parsed if you already knew the product, and the four tools were listed without saying what anyone would use them for. Lead with the wordmark and a plain claim, map real questions to tools, then explain each tool by what it refuses to do — no USD conversion in the swap search, one shared query template across three lending protocols, a frozen snapshot behind the cursor. Scope limits move from the source-scope docs onto the front page, and per-client setup covers Claude Code, OpenCode, Codex, and the mcp-remote bridge rather than one example. * docs(auth): add a guide for getting a credential Authentication was spread across the connection page, the setup guide, and the operator notes, so no single place explained that access is self-serve or that there are two ways in. Collect it into one page: the uniform key/value connection every harness accepts, the OAuth browser flow and its endpoints, minting a token directly for CI, and the operator notes on the token store and origin exemptions. Record the current client-compatibility limits rather than implying the flow works everywhere — only IPv4 loopback callbacks are accepted, only two discovery paths are answered, and the in-memory client registry does not survive a restart. The connection guide keeps per-client configuration and links here instead of repeating it. * Update README.md * docs(auth): correct how a script obtains a token The endpoint always answers in HTML and has no JSON representation, so the documented curl returned a page rather than a token. Show the scrape a script actually needs, and note that the per-address hourly limit answers 429 with a page containing no token — a scrape then yields an empty string instead of an error, which reads as an authentication failure later. * docs(readme): close the bold on the tagline The shortened tagline lost its closing markers, so the line rendered with a literal ** and left the rest of the document emphasised.
* feat(http): blur the issued token and stop capping mints The token was rendered in plain text, so it survived a screen share or a screenshot taken for any other reason. It is now blurred until pointed at or focused. The blur is presentation only: the value stays selectable underneath, so one click still selects the whole token and the page needs no script, which keeps its script-src 'none' policy intact. Minting is no longer capped per address. The cap could not limit access, since anyone refused could take a token from another address or simply wait, so in practice it only blocked someone reconnecting. Metered spend is bounded where it is actually incurred, by the request rate limiter in front of the tools. Removing it also retires the 429 page. * docs(auth): drop the mint cap and describe the blurred token Minting is no longer capped per address, so the 429 page and the advice to reuse a token rather than mint one no longer describe the server. Record that the issued token is blurred until pointed at, since a reader who does not know that will think the page failed to show it.
…nt (#58) * docs(readme): show one Cursor and Codex example instead of every client The Connect section carried a full config block for each client plus the mcp-remote bridge, which buried the fact that all of them need the same URL and header. Keep Cursor and Codex as representative examples and leave the exhaustive per-client matrix to docs/connect.md. * docs(readme): use the mcp-remote bridge form for the Claude example Swap the Cursor example for Claude and show it the way The Graph documents its own server: the mcp-remote command form, which works in both the desktop app and Claude Code. Move the shell export next to Codex, the only one of the two that reads the token from the environment.
… it (#59) A blur still shows the shape of the value and reads as a rendering glitch rather than a deliberate cover. Swap two spans instead: only one is ever in the layout, so the asterisks can never be caught in a copy and the value stays out of reach until hover or focus asks for it. Still no script, so the page keeps its own CSP.
Keep a hover/focus asterisk mask instead of blur, and remove the Shown once callout so the page is just the value and how to copy it.
* fix(nuthatch): query pool__swap without CAST for wallet and LSS Authored views and CAST(... AS VARCHAR) caused /sql HTTP 400s while freshness on pool__swap still worked. Align live scans with the indexed table, parallelize head SQL with nest/schema like compare_pools, and surface Catalog Error detail in warnings. * fix lint in wallet activity concurrency test
The Connect section said a normal user does not need "Nuthatch access", which reads as an optional extra rather than a boundary. Nuthatch is a private, unauthenticated loopback service inside the DeepTrace server with no public route, so there is nothing a client could connect to even if it wanted one.
docs(readme): note that Nuthatch is reachable only from the server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.