Skip to content

Develop - #63

Merged
kapustazh merged 117 commits into
mainfrom
develop
Jul 26, 2026
Merged

kapustazh merged 117 commits into
mainfrom
develop

Conversation

@kapustazh

Copy link
Copy Markdown
Collaborator

No description provided.

kapustazh and others added 30 commits July 25, 2026 00:54
- 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
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
* 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
* define M0 core policy

* document M0 core policy

* clarify M0 policy ownership
feat(m2): add Graph source validation pipeline
ikodo0 and others added 29 commits July 26, 2026 04:53
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.
* 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
@kapustazh
kapustazh merged commit 196ff1a into main Jul 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants