v1.0.0-rc0
Pre-releaseAdded
- API-key hashing now supports HMAC-SHA256 under a server-held pepper
(KEY_HMAC_PEPPER, optional;KEY_HMAC_PEPPER_PREVIOUSfor one
rotation window). Keys carry ahash_version; legacy SHA-256 keys
(v0) continue to authenticate unchanged via versioned candidate
lookup. SettingKEY_HMAC_PEPPER_PREVIOUSwithoutKEY_HMAC_PEPPER
fails boot. - Postgres key-store backend (
KEY_STORE_BACKEND=postgres). An
optional Postgres-backed key store for multi-replica deployments where
all replicas must share a single authoritativeapi_keystable.
File backend (KEY_STORE_BACKEND=file) remains the default and is
unchanged. New env vars:KEY_STORE_BACKEND,KEY_STORE_DSN
(required whenpostgres). Dependencies added:pgx/v5,goose/v3. - Advisory-locked boot-time migrations. goose migrations for the
Postgres backend run automatically at startup under a
pg_advisory_lock, making concurrent replica boot safe without a
separate migration job. - Persisted lazy v0→v1 rehash (Postgres backend). On first
authenticated use, a legacyv0key (plain SHA-256) is transparently
rehashed tov1(HMAC-SHA256 underKEY_HMAC_PEPPER) and the updated
digest is committed to theapi_keystable. - Hard pepper gate (
ErrPepperRequired). When
KEY_STORE_BACKEND=postgresandAUTH_PROVIDER=apikey, boot fails
immediately ifKEY_HMAC_PEPPERis unset. Closes the risk of a
misconfigured replica falling back to unprotected v0 hashing on a
shared store. - Key expiry enforcement (Phase 4).
expires_atis now enforced at
Lookup on both file and Postgres backends. A key expires when
now >= expires_at; a zero/NULLexpires_atnever expires. - Three-way reject taxonomy with bounded disclosure. Auth rejections
are classified into three messages:invalid API key(no-match —
also returned on the timing-equalised miss path so non-holders cannot
distinguish unknown-key from known-key),key expired(matched row,
expiry passed; appends— renew at <KEY_RENEWAL_URL>when set), and
key revoked(matched row,enabled=false). The specific messages
are only reachable by a caller holding the matched key's bytes. KEY_DEFAULT_TTLenv var (default8760h≈ 1 year). Applied by
the admin REST API andkey-mgmt createwhen no per-keyttl
override is given.0means no default expiry. Does not apply to the
staticMCP_API_KEYpath (always non-expiring).KEY_RENEWAL_URLenv var (optional). URL appended to thekey expiredrejection message; empty = no hint appended.- Admin REST API TTL support.
POST /admin/keysand
PATCH /admin/keys/{id}accept an optional"ttl"field (duration
string;"0"/"none"/"never"= no expiry; non-positive parsed
duration → HTTP 400). Admin responses (KeySummary) now include
expires_at; the zero-value0001-01-01T00:00:00Zmeans no expiry. key-mgmtCLI TTL and renew.key-mgmt creategains--ttl <dur|0>(default fromKEY_DEFAULT_TTL). Newkey-mgmt renew <client-id> --ttl <dur|0>subcommand extends or clears expiry from
now;--ttlis required (the default TTL is not applied on renew).
Fixes from the rc9 dmome21 full smoke test (testnet, real MCP client),
re-verified end-to-end against a local rc10 binary over the real MCP protocol.
Added
evm_call_contractaccepts an optionalfromaddress so callers can
simulate permissioned reads that checkmsg.sender; omitting it preserves
the prior zero-address behavior. (smoke F6)
Changed
- Recognized, caller-input precompile revert reasons (e.g. an oversized
checksum) now surface to the client as an actionable message instead of the
genericupstream operation failed. Reasons are drawn from a fixed allowlist
at the anchor boundary and carried by a newErrPrecompileValidation
sentinel; raw chain text (internal type paths) still collapses. (smoke F5)
Fixed
nvnm_setup_verify_hash/nvnm_setup_verify_signature: a hash/signature
mismatch is now returned as a successful result withok: falseand the
full remediation payload (challenge,expected/recovered_address,
next_actions) instead of an error. The SDK discards structured output when
a handler returns an error, so the remediation the tools were designed to
give was previously dropped over the wire. Challenge derivation is now
documented in the tool descriptions. (smoke F2)anchor_prepare_add_record: the server now rejects the empty JSON object
{}(and whitespace-only variants) formetadatawith an actionable
message, matching the anchoring precompile, which rejects it on-chain. Prior
guidance to "pass{}" was corrected in the tool schema, README, and
MetaMask guide. (smoke F3a)anchor_prepare_grant_role: tool description now states it requires the
adminrole only, matching enforcement; it previously inherited shared prose
that incorrectly advertised the writer/admin/automation roles. (smoke F4)
Fixed
anchor_prepare_add_recordinput ergonomics (rc8 E2E findings). The
anchoring precompile rejects an emptychecksum_algoormetadataand caps
checksumat 64 chars, but those constraints were undiscoverable from the
tool schema and surfaced only as opaque gas-estimation errors. Now:
checksum_algoandmetadataare marked required in the tool schema and
validated client-side (fail-loud, wrappingmissing required parameter)
before any RPC; and achecksumpassed in its natural0x-prefixed form is
stripped to the bare hex digest server-side so both representations work
(anchor_prepare_grant_role's optional record-scopingchecksumis
normalized the same way). Documented inTOOL_REFERENCE.md; the
METAMASK_GUIDE.mdandREADME.mdexamples were corrected to pass valid,
complete inputs. No on-chain behavior change — purely input validation and
normalization at the prepare boundary.anchor_get_recordshonoredregistry_id(rc8 E2E follow-on). The
precompile's records query is keyed by registry name, so aregistry_id
filter was silently ignored — the registry_id-based lookup modes advertised by
the tool returned an empty set even when the registry held records (a silent
wrong answer, not an error).GetRecordsnow resolvesregistry_id → name
internally (an explicitregistryname still wins; an unknown id fails loud
with aresolve registry_iderror). Documented the id/name interchangeability
inTOOL_REFERENCE.md. Integration tests assert id and name return the same
records and that a bad id errors.- Clarified
pagination.totalis not authoritative. The nvnm-testnet-1
precompile returnspagination.total: 0even withcountTotal=trueand
records present (a chain-side limitation, not a client decode bug — the
decode struct matches the ABI). Documented onanchor_get_recordsand
anchor_get_registriesthat callers should page using the length of the
returned slice pluslimit/offset, nottotal. evm_get_transactionnow populatesfromand reports not-found explicitly
(rc8 E2E findings). The transaction normalizer never mapped the recovered
sender, so a real mined tx came back with an emptyfrom; it is now mapped.
Separately, a well-formed but non-existent hash yielded a zero-value object
that read asis_pending: truewith an empty hash, because the not-found
guard only checked for a nil transaction (the RPC/decoder returns a non-nil
empty struct). It now also treats a missing hash as not-found (a real
transaction — pending or mined — always carries a hash) and returns
ErrTxNotFound.anchor_get_registry/anchor_get_recordsno longer leak the internal
Cosmos proto type path on a not-found. The precompile returns a raw
collections: not found … of type …mantrachain.anchoring.v1.Registryerror
for an unknown registry id; it is now mapped to the cleanErrRegistryNotFound
sentinel at the anchor-client boundary.
Security
- Tool-handler errors are now sanitized before reaching the client. Errors
returned from a tool handler are surfaced by the MCP SDK asCallToolResult
content, which bypassed the receiving middleware'sSafeForClient(that only
sees protocol-level method errors). As a result, raw upstream error text —
RPC failures, gas-estimation reverts, decode errors, internal Cosmos proto
type paths — could leak verbatim to clients. All 21 tools are now registered
through a singleaddToolwrapper that routes the handler's error through
SafeForClient: known sentinels (not-found, auth, permission, input) pass
through unchanged; everything else collapses to a generic upstream-failure
message. Internal-implementation disclosure, not credential exposure (keys and
tokens are never placed in error text), but closed as defense-in-depth. - Bumped indirect
btcdtov0.24.2(clears Dependabot HIGHGHSA-27vh-h6mc-q6g8). Not a reachable risk:govulncheckreports 0 affected and nothing imports thebtcdroot module (go-ethuses the separatebtcec/v2); the bump clears the graph-level alert.CONTRIBUTING.md§8 gained a Dependabot-vs-govulnchecktriage note for vendored deps.
Changed
- Stateless multi-replica operation (Option 0). The MCP Streamable-HTTP
handler now runs withStreamableHTTPOptions{Stateless: true}. The server no
longer keeps a per-pod session map, so any replica can serve any request and
no load-balancer session affinity is required — plain round-robin scales
throughput. - Write approval is now the client/agent's responsibility, not the server's.
The server-side MCP elicitation prompt beforeevm_send_raw_transactionwas
the only server→client request and the sole reason sessions had to be sticky;
it has been removed. Writes gate on RBAC role (writer/admin/automation)
plusENABLE_WRITE_TOOLSonly. Theinitializeinstructions now state that
the client/agent must obtain human confirmation before submitting a signed
transaction; the caller-side signature remains the security boundary.
Removed
WRITE_APPROVAL_DEFAULTenv var, the per-keywrite_approvalfield (admin
REST API, key store, andkey-mgmtCLIset-approvalcommand / create
--write-approvalflag), the FusionAuthautomation → autoapproval mapping,
and therequired/autodistinction throughout. Writes are no longer gated
by a server-side approval policy.
Migration
- Breaking, fail-loud. Startup aborts with
ErrLegacyWriteApprovalif
WRITE_APPROVAL_DEFAULTis set, and withErrLegacyKeyWriteApproval(naming
the offending key IDs) if any API-key-store entry still carries a
write_approvalfield. Remove the env var from your config and strip the
write_approvalfield from every key entry. See
docs/RUNBOOK.md#write-approval-removal. Deliberate hard cut (no silent
fallback), consistent with theINVENIAM_* → NVNM_*migration.
Version naming note. Tagged
v1.0.0-rc7(no dot), continuing the
rc4/rc5/rc6form. A dotted1.0.0-rc.7would sort before1.0.0-rc6
under SemVer pre-release precedence (see the rc6 note below), so the no-dot
form is required for correct ordering. The CHANGELOG header matches the tag.
Fixed
- Completed the "anchor precompile emits no events" privacy-claim correction
begun in rc6 across all remaining surfaces — the rc6 pass corrected only
theinitializeinstructions string andPRIVACY_DISCUSSION.mdand missed
the rest. The false claim (the precompile DOES emit events exposing the
anchored SHA-256 hash and the registry name in public logs) is now corrected
in the runtime tool strings (nvnm_overviewincl. theprivacy_by_design
response field,wallet_status,nvnm_setup_wizard), and inDESIGN.md,
README.md,TOOL_REFERENCE.md,PHASE_8_DESIGN.md, and
marketing/PRODUCT_BRIEF.md. Where a tool's limitation was justified by the
false "no events" premise, the true reason is now stated:wallet_statusand
the wizard read only balance and nonce, never transaction contents. DESIGN.mdno longer claims error messages are "never recorded in traces" —
they are attached to internal span events for debugging (sanitized before
reaching clients), matchingDATA_HANDLING.md. Removed a non-existent
error_typelabel from themcp.server.tool.errorsmetric row in
DATA_HANDLING.md(the code emitsmcp.method+mcp.tool.nameonly).
Changed
- Customer-facing surfaces that presented authentication as universal now carry
the keyless-read qualifier: underMCP_KEYLESS_READS=true(the Inveniam-hosted
default) onlyevm_send_raw_transactionauthenticates and records a per-client
identifier; theanchor_prepare_*tools are auth-exempt and anonymous reads
carry noclient_id(README.md,OVERVIEW.md,TOOL_REFERENCE.md,
OWASP_AUDIT.md,marketing/PRODUCT_BRIEF.md, and thenvnm_overview
runtime prerequisites text).
Security
- The container image now runs as the distroless non-root user (
USER 65532:65532
in the Dockerfile), so a plaindocker runmatches the non-root posture the k8s
securityContextalready enforces.
Documentation
- Recorded the hosted MCP service endpoints (
mcp-testnet.nvnmchain.io/
mcp.nvnmchain.io) in the canonical reference tables (DESIGN.md§ Target
Chain and theREADME.mdquick-reference) — they were previously only in
roadmap docs.
Version naming note. This release is tagged
v1.0.0-rc6(no dot),
continuing thev1.0.0-rc4/v1.0.0-rc5form rather than the dotted
v1.0.0-rc.Nused through rc.3. The no-dot form is deliberate: under SemVer
pre-release precedence a dotted1.0.0-rc.6would sort before the existing
1.0.0-rc5(the identifier"rc"is a prefix of"rc5", so"rc" < "rc5"),
ranking this release as older than its predecessor.rc6sorts correctly
afterrc5. The CHANGELOG header matches the tag string exactly.
Security
- Bumped the Go toolchain to 1.26.4 (
go.moddirective, Dockerfile base
imagegolang:1.26.4-alpine+ digest, andGOTOOLCHAIN) to patch two
reachable standard-library advisories surfaced bygovulncheck:
GO-2026-5039(net/textproto, used by the SMTP and admin-HTTP paths)
andGO-2026-5037(crypto/x509). No application code changes.
Changed
-
Boolean environment variables now fail loud on an unrecognized value
instead of silently coercing it to the default. All seven boolean flags
(ENABLE_WRITE_TOOLS,ENABLE_PROMETHEUS,ENABLE_STDOUT_TELEMETRY,
OTLP_INSECURE,MCP_KEYLESS_READS,NVNM_KEY_REQUEST_ENABLED,
NVNM_TRUST_PROXY_HEADERS) are parsed through a newenvBoolhelper
(strconv.ParseBoolsemantics: accepts1/t/T/TRUE/true/Trueand the
false equivalents, trims whitespace). Previously a bare== "true"
compare meantENABLE_WRITE_TOOLS=1or=Truesilently produced a
read-only server with no error; such values now abort startup with a
message naming the offending key. Validtrue/falseconfigs are
unaffected. The fiveLoad()-level flags are grouped into a new
loadFeatureFlagsparser. -
MCP_KEYLESS_READSis now set explicitly in the Helmvalues.yamland
k8sconfigmap.yaml(self-hoster defaultfalse) and documented in
RUNBOOK.mdas a requiredtrueinvariant for the Inveniam-hosted
deployment — the published privacy policy represents that deployment as
keyless-read, so runningfalsethere would falsify it. Self-hosters
choose their own posture. No code change; the env var already existed.
Fixed
-
MCP authorization-spec compliance for HTTP transport: Claude-class clients
(Claude Code / Desktop) no longer report "Needs authentication" when a valid
staticAuthorization: Bearertoken is configured. Two gaps are closed.
(1) The OAuth discovery well-known paths/.well-known/oauth-protected-resource
and/.well-known/oauth-authorization-servernow return404— via a new
wellKnownGuardahead ofAuthMiddleware(internal/mcp/wellknown.go) —
instead of falling through to a gated401. A404signals "no OAuth
discovery here, use your configured credentials"; the previous401read to
a client as "OAuth-protected resource you cannot reach." The guard matches
only those two exact paths, so/.well-known/jwks.jsonand any future
well-known resource are unaffected. (2) EveryAuthMiddleware401now
carries a plainWWW-Authenticate: Bearerchallenge (RFC 6750 / 7235) via a
newwriteUnauthorizedhelper — deliberately with noresource_metadata
parameter, because this server authenticates opaque API keys / FusionAuth
JWTs supplied out-of-band, not an OAuth flow. Credential validation, RBAC,
and rate limiting are unchanged. Server-side behavior verified end-to-end
through the full middleware chain. -
Corrected a false "non-eventful" privacy claim. The server's
initialize
instructions string (returned to every MCP client at session start) and
PRIVACY_DISCUSSION.mdstated the anchor precompile "emits no events." On-chain
inspection (eth_getLogs) showsadd_recordemits an event exposing the
anchored SHA-256 hash andadd_registryexposes the registry name in public
logs. The instructions string now states the true non-custody property (no
server-side keys; prepare-sign-submit) and that anchored data is public —
encode anything sensitive before anchoring. Supporting docs carry a dated
retraction. No behavior change beyond the instructions-string content. -
Write-approval elicitation is now MCP-spec compliant.
evm_send_raw_transaction
underwrite_approval: requiredsends its confirmation prompt via MCP
elicitation; the request previously carried only amessageand no
requestedSchema, which spec-strict clients (e.g. Claude / Fable 5) reject as
malformed — so the broadcast could never complete from those clients (reported
by an integrator whose script had to bypass the tool and write to chain
directly). The request now sendsmode: "form"and a validrequestedSchema
(an object with one booleanapproveproperty). The accept/decline/cancel
action remains the decision. The gap was invisible in CI because the go-sdk
in-process test client tolerates a nil schema; a new e2e test now asserts the
outgoing request carries a valid form schema.
Version naming note. Mantra tagged the previous release as
v1.0.0-rc4(no dot) rather than the project's priorv1.0.0-rc.3
(dotted) convention. This release returns to the dotted form for
strict SemVer-pre-release compliance and to keep CHANGELOG headers
consistent with the older entries. Both forms parse as valid SemVer;
the dot is the project's continuing convention.
Phase 11 L3 self-serve API-key request endpoint complete; Phase 10 RD3
HTTP-level error-rate SLI implemented; Phase 9.14 carried-over k8s
manifest cleanups landed (BREAKING for existing deployments — see
docs/RUNBOOK.md § "K8s manifest migration (Phase 9.14 follow-up)");
miscellaneous OSS-hygiene work including the engineering-side Terms of
Service draft, Node.js 24 action bumps ahead of the 2026-09-16 runner
cutover, and the wallet-page repo migration from inveniamcapital/
into NVNM-Chain/.
Added
internal/mcp/keys_pending.go: file-backedPendingKeyStorefor
self-serve API-key requests with atomic-write JSON persistence and
double-approve race guards. (PR #8 — Phase 11 L3 PR 1/3.)internal/mcp/keys_request_http.go: publicPOST /api/v1/keys/request
endpoint with per-source-IPKeyRequestRateLimiter, body-size cap,
email validation, and 202{request_id, status: "pending"}response
per Phase 11 RD3. New env vars:NVNM_KEY_REQUEST_ENABLED(opt-in),
NVNM_KEY_PENDING_FILE,NVNM_KEY_REQUEST_RATE_LIMIT,
NVNM_KEY_REQUEST_RATE_BURST,NVNM_KEY_REQUEST_MAX_BODY_BYTES.
(PR #9 — Phase 11 L3 PR 2/3.)internal/mcp/admin_keys_pending.go: admin pending-review endpoints
GET /admin/keys/pending,POST /admin/keys/pending/{id}/approve,
POST /admin/keys/pending/{id}/reject. Approve mints the credential,
persists the decision under a double-approve guard, and delivers the
notification email; approve response includes the issued key so
reviewers using the API directly (no SMTP) can deliver out-of-band.
(PR #10 — Phase 11 L3 PR 3/3.)internal/mcp/smtp.go: provider-agnosticEmailSenderinterface
with two implementations —SMTPEmailSender(plain SMTP with
optional PlainAuth and CR/LF header-injection defense) and
LogOnlyEmailSender(no-SMTP fallback that writes approval emails
to structured logs for operators without SMTP wiring). New env vars:
NVNM_SMTP_HOST/NVNM_SMTP_PORT/NVNM_SMTP_USERNAME/
NVNM_SMTP_PASSWORD/NVNM_SMTP_FROM/NVNM_SMTP_FROM_NAME.
Per Phase 11 RD2. (PR #10.)internal/telemetry/http_responses.go+ new
internal/mcp/response_metrics.gomiddleware: Phase 10 RD3 HTTP-
level error-rate SLI with aclasslabel on the new
mcp_http_responses_totalcounter
(server_fault/customer_impact/client_error/success).
Wired inserver.goas the outermost real-request layer (inside
CORS, outside Origin guard). Adds three Prometheus alert rules to
deploy/prometheus/alerts.yaml:NvnmMCPServerFaultRate(warn at
1% 5xx ratio),NvnmMCPServerFaultRateCritical(5%), and
NvnmMCPCustomerImpactRate(5% combined 5xx+429+408 ratio). (PR #5.)internal/config.Config.WalletGeneratorURL+ wizard hook: the
nvnm_setup_wizardneeds_walletresponse now surfaces the
browser-hosted wallet generator page (default
https://wallet.nvnmchain.io) alongside the existing snippet flow.
New env varNVNM_WALLET_GENERATOR_URL. Phase 11 D-L8-2. (PR #7.)docs/TERMS.md: engineering-side Terms of Service draft for the
hosted Service. Bakes in resolved decisions from
PHASE_11_DESIGN.md§ 14 — free-for-v1 + reserve-right-to-charge +
no-grandfather (RD4); "reasonable efforts" availability (RD5);
wallet-page out of scope (RD8); Apache 2.0 / Service bifurcation;
acceptable-use enumerated against the real tool surface. Counsel-
iteration items (jurisdiction, forum, effective date) appear in
bracketed provisional form. (PR #2.)
Changed
- BREAKING for existing k8s deployments: rename across
deploy/k8s/*— namespaceinveniam-mcp→nvnm-mcp; API-key
mount path/var/run/secrets/inveniam→/var/run/secrets/nvnm;
app.kubernetes.io/part-of: inveniam→nvnm-chainon every
manifest; phantominveniam-keymgmtreference in
networkpolicy.yamlremoved. Also fixes a Phase-9.14-era bug in
deploy/k8s/secret.yaml.example(was still naming Secrets
inveniam-mcp-server-*mismatchingdeployment.yaml, and was
still settingINVENIAM_EVM_RPC_URLwhich fails loud at startup
per Phase 8.9). Parallel-rollover migration documented in
docs/RUNBOOK.md§ "K8s manifest migration (Phase 9.14 follow-up)".
(PR #6.) .github/workflows/image.yml: bumped all 5 Docker actions to
versions on Node.js 24 ahead of the 2026-09-16 GitHub Actions
runner cutover —setup-qemu-action@v4,setup-buildx-action@v4,
metadata-action@v6,login-action@v4,build-push-action@v7.
Per-action breaking-change analysis inline as workflow comments.
Drop-in for this workflow's input shapes. (PR #4.)nvnm_setup_wizardneeds_walletresponse prose updated to
introduce the wallet-generator URL alongside the language-specific
code snippets. (PR #7.)
Operational
- Wallet-generator-page repo migrated from
inveniamcapital/nvnm-wallet-page(interim, 2026-05-27) to the
canonicalNVNM-Chain/nvnm-wallet-pagevia mirror-push of the
scaffolding commit (30cb60e; same SHA preserved). Interim repo
archived with deprecation banner. (PR #3.) docs/IMPLEMENTATION_PLAN.md: 6 backlog rows flipped to Completed
(Phase 11 L3, k8s cleanups, Node.js 20, wallet-page migration,
image.yml bumps). One row remains: marketing brief brand-positioning
review (out of engineering scope). (PR #11.)docs/RUNBOOK.md: new env-var documentation rows for
NVNM_WALLET_GENERATOR_URL, the fiveNVNM_KEY_REQUEST_*knobs,
and the sixNVNM_SMTP_*knobs.docs/PRIVACY_DISCUSSION.md§ 3 (Draft B):[TBD]customer-
onboarding PII schema replaced with the concrete L3 shape (email,
optional company, free-text intended_use) now that the endpoint
exists. The schema in code (KeyRequestInput) and the schema in
the policy now match.
Notes
- Phase 11 engineering scope is now complete. The remaining Phase 11
exit criteria (Privacy Policy counsel sign-off, Anthropic/OpenAI
directory submissions, launch announcement, support mailbox
provisioning, beta-cohort onboarding) are non-engineering scope
per the 2026-05-27 OQ walkthrough and belong with counsel /
Inveniam Comms / Inveniam Product / Inveniam HR. - Repo visibility flip from
privatetopublicis business-gated
per the Phase 9.15 plan, not engineering-gated.
First signed release from the NVNM-Chain/nvnm-mcp-server home,
validating the Phase 9.14 Cosign cert-identity path end-to-end. Helm
chart bumps 0.2.1 → 0.2.2 to track the new image-tag default. Bundles
all the work in the Unreleased section below (Phase 9.4 DCO workflow,
Phase 9.7 multi-arch image + Cosign signing, Phase 9.14 repo move +
module-path rewrite, Phase 9.16 keyless-read middleware split, plus
the 2026-05-27 OQ-walkthrough resolutions baked into the design docs).
Detail
Phase 9.14 (repo move + module-path rewrite): canonical home moved
from inveniamcapital/NVNM_MCP_Server (mixed-case placeholder org)
to NVNM-Chain/nvnm-mcp-server (lowercase-hyphen, matches the
destination org). Go module path rewritten from the vanity
placeholder github.com/inveniam/nvnm-mcp-server (which never
resolved to a real GitHub org) to
github.com/NVNM-Chain/nvnm-mcp-server (externally resolvable for
the first time). 112 occurrences across 60 files: 54 Go imports +
build/lint tooling (.golangci.yml local-prefixes, Makefile
goimports -local, ci.yml govulncheck --ignore, release.yml
ldflags -X target). Container image namespace moved from
ghcr.io/inveniamcapital/nvnm-mcp-server to
ghcr.io/nvnm-chain/nvnm-mcp-server (Image workflow IMAGE_NAME,
Helm values.yaml, k8s deployment.yaml, Helm chart README,
Phase 10 design doc, marketing brief, Privacy Policy publisher
identity table). Cosign cert-identity verification regex in release
notes updated to the new GitHub URL; prior releases
(v1.0.0-rc.1, v1.0.0-rc.2) retain their original release-page
links to inveniamcapital/NVNM_MCP_Server since the signed binary
assets and Cosign certificates were published under that identity.
Helm chart version 0.2.0 → 0.2.1 (rendered Deployment image
repository differs). Approach: fresh push to a Mantra-team-prepared
empty repo (full git history mirrored, v1.0.0-rc.1 tag preserved),
not a GitHub repo transfer; no downstream consumers existed at the
old vanity Go path. In passing, 7 broken Prometheus runbook_url
entries pointing at github.com/inveniam/NVNM_mcp_server (which had
neither a valid org nor valid repo casing) were repaired to the new
home. Five operator-facing runtime-identifier cleanups (k8s
namespace inveniam-mcp, secret mount path
/var/run/secrets/inveniam, app.kubernetes.io/part-of label,
networkpolicy comment hygiene, marketing brand-positioning audit)
deferred to the Backlog Outstanding table for their own
operator-facing migration window.
Phase 9.7 (multi-arch container image + Cosign keyless signing):
added .github/workflows/image.yml building linux/amd64 +
linux/arm64 container images via Docker buildx (QEMU
cross-compile) and pushing to GHCR
(ghcr.io/inveniamcapital/nvnm-mcp-server). Triggers: push to
main (publishes :main + :sha-<7> tags), tag v* (publishes
:<version> + :<major>.<minor> + :sha-<7>), and pull_request
(builds only, no push). Manifest digest is keyless-signed via
sigstore/cosign-installer@v3 with identity bound to
token.actions.githubusercontent.com; buildx SLSA provenance and
SBOM attestations are attached to the manifest. The Dockerfile was
already multi-arch-ready (TARGETARCH -> GOARCH). Dedicated
workflow file so QEMU's slow cross-compile path does not slow the
fast Go-test feedback loop in ci.yml. First push exercises the
path end-to-end.
Phase 9.4 (DCO sign-off CI hook): added
.github/workflows/dco.yml enforcing
Signed-off-by: Name <email> trailers on every non-merge commit in
a pull request. Failures print the offending SHAs and a fix recipe
(git commit --amend -s or git rebase --signoff). Workflow form
chosen over GitHub-App form because App installations are ephemeral
across the planned Phase 9.14 NVNM-Chain org transfer; a workflow
moves with the repo. CONTRIBUTING.md § 6 (DCO) updated to point at
the workflow file. The optional DCO GitHub App for per-commit
comment threading can be added later without changing the workflow.
Phase 9.16 (keyless-read auth middleware split): split the HTTP auth
chain so read tools can run anonymously while write tools keep their
existing auth requirement. New env vars MCP_KEYLESS_READS=false
(default), MCP_ANON_RATE_LIMIT=5, MCP_ANON_RATE_BURST=5
(HTTP-only; stdio remains all-trusted). New components:
internal/mcp/authpolicy.go introduces a fail-closed exempt-tool
registry (20 read/prepare tools exempt; only
evm_send_raw_transaction requires auth) plus an MCP receiving
middleware that rejects anonymous calls to gated tools;
internal/mcp/anonrate.go adds an AnonReadRateLimiter that throttles
anonymous traffic per source IP and bypasses authed requests.
AuthMiddleware now admits anonymous requests when the
Authorization header is fully absent (a present-but-invalid token
is still rejected). Telemetry omits client_id from logs and span
attributes on anonymous calls (absent, not empty-string) so anonymous
traffic carries no per-caller identifier. apperrors.ErrAuthRequired
added alongside ErrPermissionDenied and passed through
SafeForClient so the rejection reaches the MCP client with its
identity intact. Documented in .env.example, docs/RUNBOOK.md
env-var table, docs/DATA_HANDLING.md §§2/6/7.2. The Inveniam-hosted
Draft B privacy policy can now publish honestly (its "no per-customer
identifier on read traffic" commitment is enforced in code).
End-to-end coverage in internal/mcp/server_e2e_test.go
(TestE2E_Keyless_*). Backward-compatible: with the default
MCP_KEYLESS_READS=false, behavior is identical to pre-9.16.
Phase 9.2 (Issue + PR templates): added .github/ISSUE_TEMPLATE/
(bug_report.md, feature_request.md with a thin-proxy / no-custody
scope-fit checklist, question.md with a docs/RUNBOOK.md pre-flight)
plus config.yml (blank issues disabled; security reports routed to the
GitHub private-advisory flow per SECURITY.md rather than a raw email;
Discussions + docs contact links per the GitHub Discussions decision),
and .github/PULL_REQUEST_TEMPLATE.md (DCO sign-off, required
linked-issue/design field, docs/tests/CHANGELOG sync checklist). Honors
CONTRIBUTING.md §5's existing forward-reference to "the PR template."
Contributor-facing only; no runtime behavior change. Sequencing step 2
of Phase 9 (OSS Readiness).
Phase 9.5 (CORS middleware): added internal/mcp/CORSMiddleware, wired
as the outermost HTTP layer, so browser-hosted MCP clients can make
cross-origin requests. It shares the existing NVNM_ALLOWED_ORIGINS
allowlist but is a distinct concern from the Phase 8 Origin guard (CORS
grants browser permission; the Origin guard rejects spoofed origins —
both run). Answers OPTIONS preflight (204 with Allow-Origin,
Allow-Methods, Allow-Headers: Authorization, Content-Type, Mcp-Session-Id, Max-Age); exposes Mcp-Session-Id on actual
responses; Access-Control-Allow-Credentials: false (no cookies);
Vary: Origin when echoing. Server-to-server callers (no Origin
header) are unaffected. No new config. Built test-first. Docs:
docs/RUNBOOK.md "CORS (cross-origin browser access)" section.
Phase 9.6: Helm chart production polish. Chart bumped from 0.1.0
to 0.2.0. Hardened pod + container securityContext
(runAsNonRoot, distroless UID/GID 65532, readOnlyRootFilesystem,
all capabilities dropped, seccompProfile: RuntimeDefault); added
starter resource defaults (100m/128Mi request, 500m/256Mi
limit, documented as starter values to be re-sized in Phase 10);
default replicaCount: 2; new optional templates for
PodDisruptionBudget (gated by replicaCount > 1), NetworkPolicy
(narrow egress + scoped MCP ingress), and Ingress (off by default,
cert-manager + nginx worked example in values.yaml). New chart
README at deploy/helm/nvnm-mcp-server/README.md covering values,
mainnet-vs-testnet, and gotchas. helm lint clean; helm template
renders 4 objects at defaults and 8 with all features enabled.
Sequencing step 6 of Phase 9 (OSS Readiness); no runtime behavior
change to the server binary.
Phase 9.9 (Makefile drift cleanup): the run-local target no longer
embeds chain values; it sources .env (failing loud if absent) and
runs bin/nvnm-mcp-server --transport http, so the operator's .env
is the single source of truth (no more stale retired-testnet 58887
in the Makefile). Curl-probe targets (healthz, readyz, metrics)
now read METRICS_ADDR with a default of :9190 matching
.env.example (was hardcoded :9090); they also switched from
curl -s to curl -sSf so HTTP errors fail non-zero. The four
broken per-tool probe targets (mcp-init, mcp-chain-id,
mcp-registries, mcp-anchor-info) were removed and replaced by a
single parameterized make mcp-probe TOOL=<name> ARGS='<json>' that
performs the full initialize -> Mcp-Session-Id capture ->
notifications/initialized -> tools/call handshake inline against
MCP_HTTP_ADDR (default :8180); pretty-prints with jq when
available. New make mcp-probe-help prints example usages. Help
text in make help updated accordingly. Makefile only; no behavior
change in the server.
Phase 9.12: mainnet cutover playbook landed at docs/MAINNET_CUTOVER.md.
Documents the testnet → mainnet config diff (NVNM_EVM_RPC_URL,
NVNM_CHAIN_ID, NVNM_CHAIN_ENVIRONMENT), validation sequence,
rollback path, and the open precompile-pagination question parked for
Phase 10 staging. Doc only; execution is Phase 10. Sequencing step 12
of Phase 9 (OSS Readiness); no behavior change.
Phase 9.3: per-file SPDX license headers added to every .go file
under cmd/ and internal/ (100 files). Mechanical bulk rewrite
recorded in .git-blame-ignore-revs; CI lint enforces the header on
future additions. Sequencing step 3 of Phase 9 (OSS Readiness); no
behavior change.
Phase 9.1: OSS foundation documents shipped (LICENSE, NOTICE,
CODE_OF_CONDUCT, CONTRIBUTING, SECURITY). Sequencing step 1 of Phase 9
(OSS Readiness); no behavior change.
Phase 9 prep (PR #23): surface a server-level instructions string
in the MCP initialize response so first-contact agents receive the
lobby pointer and the privacy-by-design caveat at session start, even
if their client compresses or omits tool descriptions.
Dependency bumps (2026-05-18): three workflow / base-image bumps
landed via Dependabot (golang 1.26.2 -> 1.26.3 alpine,
actions/download-artifact v7 -> v8, softprops/action-gh-release v2
-> v3). Plus a manual go-sdk bump 1.5.0 -> 1.6.0 (this PR) after
Dependabot's auto-rebase repeatedly failed on stale vendor/ state.
Phase 8.9: hard cut from the legacy INVENIAM_* env-var prefix to
NVNM_* and matching server-identity rename. Single coordinated
BREAKING change.
Changed
Phase 9.6: Helm chart production polish
deploy/helm/nvnm-mcp-server/Chart.yaml: chartversionbumped
from0.1.0to0.2.0(semver-meaningful chart change:
added templates, new values keys, hardened defaults).appVersion
left at0.5.0to matchvalues.image.tag; Phase 10 will retag
against the canonical app version in
internal/version/version.goat
the next multi-arch release.deploy/helm/nvnm-mcp-server/values.yaml: structural rewrite
with section-level comments explaining intent and override
guidance. Notable defaults:replicaCount: 2(minimum redundancy during voluntary
disruptions; one pod cannot survive a node drain).resources.requests: {cpu: 100m, memory: 128Mi}and
resources.limits: {cpu: 500m, memory: 256Mi}. Documented as
starter values aimed at staging; real capacity planning is
Phase 10. Override per-environment.podDisruptionBudget.enabled: true(with the rendering
double-gated byreplicaCount > 1-- a one-pod deployment
cannot meaningfully satisfyminAvailable=1).networkPolicy.enabled: false(off because not every CNI
enforces NetworkPolicy; flip on for Calico / Cilium / etc.).
Egress pre-populated for HTTPS (EVM RPC + FusionAuth JWKS),
OTLP gRPC (4317), and DNS.ingress.enabled: falsewith a worked cert-manager +
nginx-ingress example commented in-file.- Pod- and container-level
securityContextblocks expanded to
coverrunAsNonRoot: true,runAsUser/Group: 65532,
fsGroup: 65532,allowPrivilegeEscalation: false,
readOnlyRootFilesystem: true,capabilities.drop: [ALL],
andseccompProfile.type: RuntimeDefault. UID 65532 matches
the distrolessnonrootconvention used by
gcr.io/distroless/static-debian12(see Dockerfile).
deploy/helm/nvnm-mcp-server/templates/deployment.yaml: pod-
levelsecurityContextnow renders the full block viatoYaml
so additions invalues.yaml(notablyfsGroupand
seccompProfile) flow through without template edits.
Validation: helm lint clean (only the informational "icon is
recommended" hint); helm template smoke-tested in two modes
(default-values render produces 4 objects: ConfigMap, Service,
Deployment, PDB; full-feature render with NetworkPolicy + Ingress
- HPA + ServiceMonitor enabled produces 8). No runtime behavior
change to the server binary. New templates (PDB, NetworkPolicy,
Ingress) are catalogued under### Addedbelow.
Phase 9.11: README polish for OSS audience
- Added a one-paragraph elevator above the existing first lines: what
the server is, who it is for, and why an unfamiliar reader should
care. Existing intro paragraph preserved underneath. - Added a status-badge row at the top: CI status (GitHub Actions on
main), license (Apache 2.0), latest release (shields.io GitHub
release tag), Cosign-signed (release pipeline). - Added an ASCII "Request Flow" diagram of the HTTP middleware chain
(originGuard->failGuarded->limitRequestBody->
AuthMiddleware->rateLimitMiddleware-> MCP SDK -> tool handler
-> EVM client). Sourced verbatim from
internal/mcp/server.go. - Added a "Documentation" link tree pointing at the OSS foundation
files (LICENSE,NOTICE,CONTRIBUTING.md,CODE_OF_CONDUCT.md,
SECURITY.md,CHANGELOG.md) and the deeper technical references
(docs/DESIGN.md,docs/RUNBOOK.md,docs/SECURITY_AUDIT.md,
docs/DATA_HANDLING.md,docs/KEY_CUSTODY_THREAT_MODEL.md,
docs/TOOL_REFERENCE.md,docs/IMPLEMENTATION_PLAN.md). - Added a "What this server is not" scope-statement section that
mirrorsCLAUDE.md's private list (not a chain node, not a wallet,
not a custodian, not an orchestrator), adapted to public-audience
wording. - Drive-by: flipped the License footer from the stale "Proprietary.
All rights reserved." to Apache 2.0 with a pointer to the LICENSE
file shipped by Phase 9.1.
No behavior change. Status paragraph, tools listing, configuration
env-var tables, and docs/ structure listing left untouched per
Phase 9.11 scope.
Bump github.com/modelcontextprotocol/go-sdk 1.5.0 -> 1.6.0
- Direct dep
github.com/modelcontextprotocol/go-sdkupgraded from
1.5.0 to 1.6.0. Transitive:github.com/google/jsonschema-go
0.4.2 -> 0.4.3. vendor/regenerated; 91 files changed (-1289 net lines as the
vendor tree contracted around upstream cleanup).- Manually applied because Dependabot's PR #19 auto-update repeatedly
failed at the dependency-resolution level ("Dependabot failed to
update your dependencies"). #19 is closed in favor of this PR.
Two upstream behavior changes in 1.6.0 were reviewed against this
codebase before merge:
- Default cross-origin protection moved from on to off in the
SDK. Verified non-applicable: our ownoriginGuardmiddleware sits
at the outermost position in the HTTP handler chain (see
internal/mcp/server.go:183),
before the SDK ever sees a request. The SDK-level default change
is independent of our enforcement. SetErrorno longer overwritesCallToolResult.Content.
Verified non-applicable:grep -rn "SetError\b" --include='*.go' cmd/ internal/returns zero matches. We do not callSetError
anywhere.
Added
Phase 9.6: New Helm templates
deploy/helm/nvnm-mcp-server/templates/poddisruptionbudget.yaml--
optional PDB atminAvailable: 1. Gated by
podDisruptionBudget.enabledANDreplicaCount > 1so the
template does not render an undrainable single-pod PDB by
default.deploy/helm/nvnm-mcp-server/templates/networkpolicy.yaml--
optionalnetworking.k8s.io/v1 NetworkPolicy. Ingress opens the
metrics port (9090) for in-cluster scrapers + kubelet probes and
scopes the MCP port (8080) to peers listed in
networkPolicy.ingressFrom. Egress is narrowed to HTTPS, OTLP
gRPC, and DNS by default; overridenetworkPolicy.egressPorts
for tighter rules. The admin REST listener (default loopback) is
intentionally omitted; the inline comment points operators at
the example admin rule in
deploy/k8s/networkpolicy.yaml.deploy/helm/nvnm-mcp-server/templates/ingress.yaml-- optional
networking.k8s.io/v1 Ingress. Off by default; the chart does
not presuppose an ingress controller. Worked example in the
values.yamlcomments uses cert-manager + nginx.
Mainnet cutover playbook (Phase 9.12)
docs/MAINNET_CUTOVER.md-- new operator-facing playbook for moving a
Helm/k8s deployment from testnet (nvnm-testnet-1/787111) to mainnet
(nvnm-1/1611). Five sections: preconditions (RPC reachable,
precompile present, FusionAuth if used, DNS, legacyINVENIAM_*
hygiene), config changes (the three pinned env vars + ConfigMap and Helm
diffs), validation sequence (pre-cutover sandbox + post-cutover
production probes againstnvnm_overview,wallet_status,anchor_info,
readyz, andevm.rpc.errors), rollback (revert + redeploy; no
on-chain unwind needed), and the open question about mainnet precompile
pagination parked for Phase 10 staging.README.md:docs/listing updated to includeMAINNET_CUTOVER.md.docs/IMPLEMENTATION_PLAN.md§ 9.12: marked DONE.
Per-file SPDX license headers (Phase 9.3)
- Every
.gofile undercmd/andinternal/now carries a
two-line SPDX header (SPDX-License-Identifier: Apache-2.0- copyright). 100 files;
vendor/excluded.
- copyright). 100 files;
scripts/add_license_headers.sh-- idempotent prepend; safe to
re-run.scripts/check_license_headers.sh-- CI guard invoked from
.github/workflows/ci.yml's new "License headers" step; fails
the build if any.gofile undercmd/orinternal/is
missing the header..git-blame-ignore-revs-- created at repo root with the
rewrite commit's full hash. GitHub's blame view honors this
automatically; local users opt in viagit config blame.ignoreRevsFile .git-blame-ignore-revs(documented in
CONTRIBUTING.md§ 5).
OSS foundation documents (Phase 9.1)
LICENSE-- standard Apache 2.0 text (canonical SHA-256
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30,
no trailing whitespace).NOTICE-- minimal Apache attribution declaring Inveniam Capital
Partners as the original copyright holder.CODE_OF_CONDUCT.md-- Contributor Covenant 2.1, verbatim.
Enforcement contact reserved with placeholder
<EMAIL_TBD:conduct@nvnmchain.io>until the alias is provisioned.CONTRIBUTING.md-- dev setup, build & test, test layers,
commit/PR norms, DCO sign-off requirement, security-disclosure
policy, and vendor-directory rules. 8 sections per
docs/PHASE_9_DESIGN.md§ 3.2. Maintainer contact reserved with
placeholder<EMAIL_TBD:maintainers@nvnmchain.io>.SECURITY.md-- private-disclosure path (GitHub Security Advisories
primary;<EMAIL_TBD:security@nvnmchain.io>fallback). Enumerates
deliberate hardening invariants so researchers don't report design
properties as bugs. Response SLO: 3-day acknowledgement, 7-day
initial severity assessment, 90-day coordinated disclosure window.
Explicit "no monetary bug bounty" per Phase 9 decision D4..env.example-- starter template covering required chain config,
HTTP transport, both auth providers, write-tool gating, integration-
test credentials, and observability. All sensitive fields are
PLACEHOLDER;.envitself is gitignored.
All mail-alias references use the disambiguating placeholder form
<EMAIL_TBD:foo@nvnmchain.io> so eventual substitution is mechanical
(sed -i 's/<EMAIL_TBD:foo@nvnmchain.io>/foo@nvnmchain.io/g').
Pre-merge grep for the literal substring EMAIL_TBD still catches
every occurrence and is a Phase 9 exit criterion before the public
repo flip (Phase 9.15).
MCP initialize-response instructions string (PR #23)
internal/mcp/server.go:NewServernow passes a populated
mcp.ServerOptions{Instructions: ...}to the SDK constructor
(previouslynil). The SDK propagates the string into the
initializeresponse per the MCP spec field
InitializeResult.instructions; clients are expected to treat it
like a system-prompt hint the model sees before any tool
description is processed.- Content: a three-sentence orientation that names the server, states
the no-events privacy property, and points first-time agents at
nvnm_overviewfor the canonical six-step journey. Kept terse on
purpose -- the richer chain summary, prereqs, and journey still
live in thenvnm_overviewtool response; the instructions string
points at that tool rather than duplicating it. Future changes to
the chain summary or journey only need to update one place
(tools_overview.go). - Why: defense in depth for the lobby-tool pattern. The existing
design relies on the agent noticing "Call this first if you have
never used this server before" inside thenvnm_overviewtool
description; that works in clients that surface full descriptions
to the model but fails silently in clients that compress them or
feed tool names only. The instructions field lands at the protocol
level and is much more likely to be read before the agent picks a
tool. Same reasoning that motivated repeating the privacy caveat
infunded_activewizard messages -- instructions is just the
highest-leverage place to repeat it. - Test:
TestE2E_Initialize_IncludesInstructionsasserts the field
is non-empty and contains bothnvnm_overviewandemits no events. Wording-stable (substring check, not full-string match)
so future copy edits do not break the test.
Changed (BREAKING)
Env var prefix: INVENIAM_* -> NVNM_* (hard cut, no alias)
- All chain/RPC config keys renamed. The three legacy keys
(INVENIAM_EVM_RPC_URL,INVENIAM_EVM_ARCHIVE_RPC_URL,
INVENIAM_CHAIN_ID) are no longer read. New names per
docs/RUNBOOK.md#env-var-migration:
NVNM_EVM_RPC_URL,NVNM_EVM_ARCHIVE_RPC_URL,NVNM_CHAIN_ID. Config.Loadruns a pre-validation pass that scansos.Environ()
for any of the three legacy keys. If any are present the server
exits immediately withErrLegacyEnvVarsand a pointer to the
migration runbook. The check fires even when the matching
NVNM_*is also set -- dual-populated config is the silent-drift
trap fail-loud exists to catch. An operator who thinks they
migrated but left a staleINVENIAM_*in a ConfigMap hears about
it on the next deploy, not when someone later unsetsNVNM_*and
the legacy value silently takes over. The strict policy was
chosen 2026-05-13 over the original "only fail ifNVNM_*is
unset" design wording. SeeCLAUDE.md"Migration hygiene principle"
for the broader rule this case follows.- Test:
TestLoad_RejectsLegacyEnvVars(table-driven across all
three keys, with validNVNM_*also set) asserts the strict
policy.
Server identity rename
- MCP
serverNameconstant:inveniam-evm->nvnm-chain. Visible
in theinitializeresponse and any client-side server-tab label. OTEL_SERVICE_NAMEdefault:inveniam-mcp-server->nvnm-mcp-server.- Internal OTel
TracerandMeternames:inveniam-mcp-server->
nvnm-mcp-server. Dashboards that filter byservice.name,
tracer, ormeterneed their queries updated; metrics keep
flowing, just under different labels. - Helm chart directory:
deploy/helm/inveniam-mcp-server/->
deploy/helm/nvnm-mcp-server/.Chart.yamlnamefield +
every template-helper include updated to match. (The image
repository invalues.yamlwas carried in this Phase 8.9 commit
as a deferred item and renamed in 8.13 below as part of the
atomic binary + Docker artifact rename.) - Kubernetes ConfigMap (
deploy/k8s/configmap.yaml) and Helm
values.yamlmigrated toNVNM_*keys; ConfigMap chain ID stays
at787111(current testnet,nvnm-testnet-1). Makefilerun-local,docker-run, anddocker-smoketargets
updated to setNVNM_*env vars.
Binary + Docker artifact identity rename (Phase 8.13)
cmd/inveniam-mcp-server/->cmd/nvnm-mcp-server/. The Go
module path (github.com/inveniam/nvnm-mcp-server) already
matched the new name, so no import-path edits were needed.- Binary name:
MakefileBINARY_NAME/CMD_DIR/DOCKER_IMAGE,
Dockerfilebuild output +ENTRYPOINT, and CI build paths now
producenvnm-mcp-server. Release artifacts published from
.github/workflows/release.ymluse the new name template
(nvnm-mcp-server-${TAG}-<os>-<arch>), with matching cosign
verification instructions in the release-notes body. - Published Docker image path: Helm
values.yamlimage.repository
is nowghcr.io/inveniamcapital/nvnm-mcp-server. The first
release tag pushed after this commit publishes to the new path;
the oldinveniamcapital/inveniam-mcp-serverimage stays in
ghcr but receives no further updates. Operators pinning to the
old image must either flipimage.repository(Helm) or update
theimage:field in their K8sDeploymentmanifest. - K8s manifests (
deploy/k8s/*.yaml): every
app.kubernetes.io/namelabel, container name, and resource
metadata.name(Deployment / Service / HPA / ServiceMonitor /
NetworkPolicy / Namespace label) renamed; image reference flipped
to the new ghcr path; ConfigMap and Secret references renamed
(inveniam-mcp-server-config->nvnm-mcp-server-config,
inveniam-mcp-server-keys->nvnm-mcp-server-keys,
inveniam-mcp-server-secret->nvnm-mcp-server-secret). - Prometheus alerts (
deploy/prometheus/alerts.yaml): alert group
name, everyapp:label, the scrape job filter
(up{job="..."} == 0), and the descriptive prose all renamed. - Grafana dashboard (
deploy/grafana/dashboard.json):titleand
uidupdated. Operator action: the dashboarduidchange
breaks existing direct URLs (/d/inveniam-mcp-server/...) --
re-import the dashboard (or update the paneluidin your
provisioning files) to pick up the new identifier. - Internal:
internal/evm/tracing.goevmTracerNameconstant;
startup log line incmd/nvnm-mcp-server/main.go.
Migration
Env vars (8.9). Operators must rename INVENIAM_* to NVNM_*
in every layer that sets chain config -- ConfigMaps, Helm overlays,
.env files, systemd units, Compose files, Terraform, shell
wrappers. Don't keep the old key alongside the new one -- the
server refuses to start when it sees both. Migration table and
worked example in docs/RUNBOOK.md#env-var-migration.
Binary + Docker image (8.13). No automatic detection, just
path changes:
- Binary on PATH:
inveniam-mcp-server->nvnm-mcp-server
(wrapper scripts, systemdExecStart, MCP client launchers,
CI invocations). - Docker image:
ghcr.io/inveniamcapital/inveniam-mcp-server:*->
ghcr.io/inveniamcapital/nvnm-mcp-server:*. The old path stops
receiving updates after this release; pin by digest if you need
the pre-rename image for rollback testing. - K8s resources: if you applied the previous
deploy/k8s/*.yaml
manifests, the renamedmetadata.namefields mean a
kubectl apply -k deploy/k8s/will create new resources rather
than mutating the old ones. Plan a deliberate cutover (parallel
deploy + traffic shift, or scheduled downtime + delete + apply). - Grafana: re-import or update provisioning to pick up the new
dashboarduid(nvnm-mcp-server); direct/d/inveniam-mcp-server/
URLs will 404.
Phase 9.16 (part 1 — FusionAuth client_id privacy): the FusionAuth JWT
sub is no longer logged. It is now hashed into client_id via a keyed
HMAC-SHA256 (MCP_CLIENT_ID_HMAC_KEY), so the logged identifier is stable
for audit correlation but not reversible to a real-world identity without
the server-held key; and the former DEBUG subject log line was removed
entirely. Breaking for FusionAuth deployments: MCP_CLIENT_ID_HMAC_KEY
is now REQUIRED when AUTH_PROVIDER=fusionauth — startup fails loud if
unset (at both config validation and validator construction). client_id
values change from the raw sub to its HMAC, so historical log
correlation across this upgrade is broken (acceptable). apikey deployments
are unaffected. Implements privacy-policy decisions §2.1 D4/D9. (Remaining
9.16 work — the keyless-read middleware split — lands separately.)
Phase 8.1-8.8 ship: tool annotations + next_actions envelope across
all tools, EIP-1559 default, Origin-header validation, API-key hashing
migration, five new onboarding tools, plus the defiweb/go-eth swap
and K8s Secret pattern cleanup.
Added
MCP tool surface (16 -> 21 tools)
- Five onboarding tools (Phase 8.8):
nvnm_overview(closed-world read) -- static lobby tool. Returns
chain identity (name, env, ID, precompile, explorer/docs/bridge
URLs, env-aware native/wrapped token naming), a 2-3 sentence
"what is NVNM Chain" blurb that includes the privacy-by-design
property, a 6-step canonical agent journey, andnext_actions
pointing atnvnm_setup_wizard. No chain calls.wallet_status(open-world read) -- one-shot snapshot of an EVM
address. Returns balance (wei + env-aware human form in
wmmUSD/wmantraUSD), pending nonce,has_sent_tx, and one of
three honest status values:unfunded,funded_unused,
funded_active.funded_activemeans "has sent any tx," NOT
"has anchored" -- the chain emits no events by design.nvnm_setup_wizard(open-world read) -- four-state prose-guided
flow:needs_wallet(samples for Python/JS/Go that store keys
viakeyring/ mode-0o600.env/os.WriteFilemode 0o600,
never print them);unfunded(bridge instructions);
funded_unused(optional verify_hash + verify_signature
challenges);funded_active(usage patterns + anchor-prepare
pointers, with explicit "any tx, not anchored" caveat).nvnm_setup_verify_hash+nvnm_setup_verify_signature(both
closed-world read, pure compute) -- stateless verification
helpers. Challenge is
sha256(lower(address) + ":" + protocol-version-tag)so the
server can recompute the expected value from the address alone
(no per-call state, no time dependence). Signature path uses
EIP-191 personal_sign via defiweb'sECRecoverer.RecoverMessage.
- A unit test asserts wizard sample code uses an allowlisted
safe-storage primitive per language; if a future contributor
changes a sample to print the key the test fails.
MCP ToolAnnotations on every tool (Phase 8.2)
- Three annotation constructors in
internal/mcp/annotations.go
(newOpenWorldReadOnly,newClosedWorldReadOnly,
newDestructiveWriteTool) return a fresh*mcp.ToolAnnotations
per call -- no shared singleton pointers (PR #20 Q4). - All 21 tools carry explicit
Title,Description, and
Annotationswith an explicitOpenWorldHint.evm_send_raw_-
transactionlocked in asDestructiveHint=true,
OpenWorldHint=true. Regression tests prevent silent relaxation.
next_actions envelope on every tool (Phase 8.3)
- Per-tool hint builders in
internal/mcp/next_actions.go. Most
return static hints; data-dependent ones branch on receipt status,
bytecode presence, empty registries, and (for
evm_send_raw_transaction) echo the tx hash into the receipt-poll
hint so the agent has a copy-pasteable next call. - Envelope structs in
internal/mcp/envelopes.goembed the
underlying response types and addNextActions []NextAction.
JSON shape stays backwards-compatible via field promotion. - AST reachability test parses every non-test
.gofile in the
package, collects everyTool:literal that appears inside a
NextActioncomposite literal, and asserts each name is in the
registered tool set. Catches typos and stale references in every
branch of every builder.
EIP-1559 prepare-tools (Phase 8.4)
anchor_prepare_add_registry,anchor_prepare_add_record, and
anchor_prepare_grant_rolenow build type-2 transactions by
default.MaxFeePerGas = 2 * SuggestGasPrice(2x headroom against
baseFee inflation),MaxPriorityFeePerGas = SuggestGasTipCapwith
a 1-gwei fallback when the RPC returns zero or errors.GasPrice
dual-populated (= MaxFeePerGas) so legacy-only signers still
have a usable value.UnsignedTransactiongainsType,MaxFeePerGas, and
MaxPriorityFeePerGas(allomitempty-- type-0 responses
preserve the legacy JSON shape exactly).WalletTransactionRequest
gains the same fee fields;GasPricebecomesomitemptyso
MetaMask et al. prefer EIP-1559 fields when present.prefer_legacy_txopt-out parameter on each prepare tool flips
back to the type-0 builder.Client.SuggestGasTipCap(ctx)added to the EVM client interface
(base, resilient, and tracing wrappers).- New golden fixture
unsigned_transaction_eip1559.golden.json;
testnet integration round-trips for both type-2 and type-0
signed-and-broadcast paths.
Origin-header validation (Phase 8.5)
internal/mcp/origin.go--OriginAllowlist,originGuard
middleware. DNS-rebinding defense per the MCP specification.
Requests with anOriginheader must match the allowlist or get
403; requests without anOriginheader (server-to-server, CLI,
curl) pass through unchanged.- Origin guard installed at the outermost middleware position
(before auth, body limit, rate limiter) so rejection
short-circuits before any expensive work. - Default allowlist covers both
http://andhttps://for
localhost,127.0.0.1, and[::1]. Loopback hosts accept any
port; non-loopback entries require exact-match including port to
defeat patterns likehttp://localhost.attacker.tld. NVNM_ALLOWED_ORIGINSenv var (comma-separated) overrides the
default. Startup log line surfaces the resolved allowlist.
Foundation types (Phase 8.1)
internal/mcp/types.go-- sharedNextActiontype embedded in
tool responses.internal/mcp/runtime.go--RuntimeInfo+RuntimeInfoFromConfig
bundle (chain env, anchor address, explorer/docs/bridge URLs)
consumed by the onboarding tools.internal/config/environment.go--ChainEnvironmentenum
(testnet/mainnet),TokenNaming,NamingFor(env), and
InferEnvironmentFromChainID.
Pre-red-team security hardening
- Pre-auth IP failure-rate limiter
(internal/mcp/failrate.go). Outermost middleware after the
origin check;AuthMiddlewarecallsPenalizeon every 401, so
credential stuffing now hits a 429 instead of unlimited attempts.
Trust-X-Forwarded-For is opt-in viaNVNM_TRUST_PROXY_HEADERS. - Per-client rate-limiter map is bounded via LRU eviction and a TTL
janitor; same pattern applied to the new IP failure limiter. - API-key store writes are atomic (
tmp + fsync + rename); the
previous file remains intact if any step fails. - Admin server hashes both sides of the bearer compare with SHA-256
beforesubtle.ConstantTimeCompare, so the length-mismatch
shortcut cannot probe the admin key's length. All bearer failures
now return 401 per RFC 7235 (previously 403). - Approval prompt decodes the recovered signer address and the
first 4 bytes of calldata (method selector), shows wei with
thousand separators and a 6-decimal ETH approximation, and renders
the chain ID with a human label ("testnet"/"mainnet"). - All five write-tool audit lines emit a structured
slog.Group
with stabletool/phase/client_idkeys so SIEM rules
don't string-match on the message body. parsehexseed corpus added for fuzz testing.
Documentation
docs/DESIGN.md§ 8 Deployment Topology gains a "Multi-chain
(testnet + mainnet)" subsection: two instances, one per chain, NOT
per-session selection within a single instance. Records the six
reasons (blast-radius isolation, audit-trail clarity, per-chain
RBAC, per-tier ops, existing startup-env config model, small
agent-UX cost) plus a "revisit triggers" list.docs/SECURITY_AUDIT.mdgains the "Update 2026-05-12: Fresh
pre-red-team review and remediation" and "Update 2026-05-13:
Phase 8.6 and 8.7 (hashed-at-rest, constant-time auth)"
sections covering the pre-red-team remediation log and the
8.6/8.7 storage + validator design.docs/SECURITY_CONSUMER_GUIDANCE.md(new) describes the two
threats the server deliberately does NOT mitigate at its boundary
-- indirect prompt injection via on-chain string fields and
approval-substitution via swapped signed-tx bytes -- and what
consuming agents should do.docs/RUNBOOK.md§ 9 documents the Phase 8.6 keys-file migration
upgrade procedure (before/during/after signals, flock multi-process
safety, rollback via.pre-migrationrestore).docs/LICENSE_EXCEPTIONS.md(new) tracks documented license
exceptions.CLAUDE.md(new) project-specific session context: chain-ID
history, "Inveniam Chain != MANTRA dukong" disambiguation table,
multi-chain deployment model, tool surface, test layout.
Changed
Tool-surface internals
- Breaking (Go API):
NewServersignature refactored to take
*config.Configinstead of individual scalars (evmClient,
anchorClient, enableWrite, writeApproval, chainEnvironment,
middleware, logger->evmClient, anchorClient, cfg, middleware,
logger). The onboarding tools needChainID,AnchorAddress,
ExplorerURL,DocsURL, andBridgeURLfrom cfg; callers
outsidecmd/inveniam-mcp-server/main.gomust update.
EVM client
- Replaced go-ethereum with
github.com/defiweb/go-ethv0.7.0
(MIT). Removes a GPL-3.0 / LGPL-3.0 dependency under the
project's proprietary commercial license policy and shrinks the
dep surface significantly. A build-tagged differential test
imported both libraries and asserted byte-for-byte ABI calldata
equality across 13 cases (addRegistry / addRecord / grantRole)
before the go-ethereum import was removed. - Surface changes:
common.Address/Hash-> defitypes equivalents
(newinternal/evm/addrhex.gopreserves EIP-55 output);
types.Transaction->defitypes.Transactionfluent builder +
defiwallet.PrivateKey.SignTransaction+
deficrypto.ECRecoverer.RecoverTransaction;ethclient.Client
->rpc.Client+transport.NewHTTP;accounts/abi.ABI->
defiabi.Contractwithabi:"..."field tags;
ethereum.CallMsg/FilterQuery->defitypes.Call/
defitypes.FilterLogsQuery. - Vendored (~32 MB under
vendor/); CI uses-mod=vendor.
pre-commit excludesvendor/from Go fmt/vet/imports/lint hooks. - License allowlist tightened to permissive-only (
MIT,BSD-2,
BSD-3,ISC,Apache-2.0,MPL-2.0, etc.). GPL-3.0 and
LGPL-3.0 removed.
Resilient client
- Recognizes the Cosmos-EVM
eth_gasPrice-> "failed to get receipts
from comet block" race as transient and retries it. Matched via a
named constant so a future upstream wording change is caught by a
unit test rather than by flaky CI. - Integration test helpers now wire
evm.NewResilientClientover
the bareevm.NewClient(production parity); receipt-poll budget
bumped from 30s to 60s to cover observed worst-case latency.
K8s manifests
- New
deploy/k8s/secret.yaml.exampledemonstrates the Secret
pattern: one Secret forINVENIAM_EVM_RPC_URL/MCP_API_KEY/
ADMIN_API_KEY/ FusionAuth IDs (envFrom secretRef), a second
Secret for theMCP_API_KEYS_FILEJSON payload (mounted as a
read-only volume,defaultMode=256). The ConfigMap stops carrying
secret-shaped fields. deployment.yamlpulls bothconfigMapRefandsecretRef;
optional flags let FusionAuth-only deploys skip the keys Secret.networkpolicy.yamldocuments why:8081is intentionally NOT in
the ingress list (admin server binds to loopback by default;
operators who flip it must add a narrow podSelector + namespace-
Selector rule).configmap.yaml: chain ID corrected from58887to787111
(current Inveniam testnet); sensitive fields removed;
NVNM_ALLOWED_ORIGINSandNVNM_TRUST_PROXY_HEADERSsurfaced as
commented examples..gitignoreexcludesdeploy/k8s/secret.yaml.
Build / toolchain
- Go toolchain bumped to 1.26.3 (govulncheck).
GOTOOLCHAINpinned
in the Dockerfile build stage to matchgo.mod-- reproducible
builds. golang.org/x/syspromoted from indirect to direct (used for
unix.Flockin the keys-file writer).
Security
API-key hashing migration (Phase 8.6, IRREVERSIBLE)
- Storage migrated from raw bearer tokens to sha256 at rest.
KeyEntrygainsKeyHash(sha256 hex) andKeyPrefix; rawKey
retained as a load-only legacy field withomitempty, cleared
after migration. The pre-8.6SECURITY_AUDIT.mdclaimed "hashed
at rest" but the code did not match; this release makes the
claim accurate. NewKeyEntry(id, rawKey, writeApproval, roles)is the sole
production constructor (hashes once, captures prefix, never
retains raw key). DirectKeyEntryliterals withKey:set are
confined to migration helpers and the migration regression test.KeyStore.byHashmap replacesbyKey.Lookup(rawKey)hashes
before probe.SaveKeysFileaddsflock(LOCK_EX|LOCK_NB)on top of the atomic
tmp + fsync + rename.LoadKeysFilefalls back to<path>.tmp
on parse failure for interrupted-write recovery.NewManagedKeyStorewrites a one-shot<path>.pre-migration
backup BEFORE any mutation (never overwritten on subsequent
migrations), normalizes in-memory entries, opportunistically
re-saves (INFO on success, WARN-and-continue on save failure).
Seedocs/RUNBOOK.md§ 9 for the operator upgrade procedure and
rollback path.internal/auth.HashKey(rawKey)shared between storage migration
and validator compare so both sides cannot drift.
Constant-time validator on hash bytes (Phase 8.7)
Validatehashes the input, looks up by hash, then verifies with
subtle.ConstantTimeCompareon fixed-length sha256 hex digests.
The previous compare against rawentry.Keywas a placebo (the
map probe used the same raw bytes); the new compare is genuine
defense-in-depth.- Miss path burns a placeholder
ConstantTimeCompareto flatten
hit/miss timing. KeyResult.Keyremoved;KeyResult.KeyHashadded. Raw key never
exposed across the package boundary.
Other security changes
- Breaking: HTTP transport fails closed when no auth validator
can be built. Previously logged WARN and ran unauthenticated; now
returnsconfig.ErrHTTPAuthRequiredand refuses to start. - Breaking: Admin REST API now binds to
127.0.0.1:8081by
default. The admin key is the master key; cluster-wide exposure
was a privilege-escalation foot-gun. Operators who need
cross-pod access must flip the bind explicitly AND add a narrow
NetworkPolicy rule. - Breaking:
OTLP_INSECUREdefault flipped. Operators
exporting OTLP over plaintext must now opt in explicitly. See
docs/SECURITY_AUDIT.md"Update 2026-05-12: Fresh pre-red-team
review and remediation". - Breaking:
NVNM_CHAIN_ENVIRONMENTis required when the
configured chain ID is not one of the recognized testnet/mainnet
IDs. Private forks must set it explicitly; recognized IDs still
infer the environment.
Fixed
- Approval prompt previously rendered an opaque hex chain ID; now
threads a human label ("testnet"/"mainnet") throughNewServer. - Telemetry middleware comment corrected: errors ARE recorded in
OpenTelemetry span events; only the response to the client is
sanitized viaapperrors.SafeForClient. - Pre-existing
govetshadow declarations of the package-level
ctxin several test files (approval_test.go,
ratelimit_test.go,rbac_test.go) renamed totCtx/authCtx. gosecG115 on theint(f.Fd())syscall cast in
internal/mcp/keys.go-- suppressed for the CI lint version
(golangci-lint v2.11.4) while local v2.12 does not flag it.
Documented divergence; matches the prior pattern.
Removed
github.com/ethereum/go-ethereumand all its transitive
dependencies. See the Changed section above for the
defiweb/go-eth swap.KeyResult.Key(raw bearer token) -- replaced by
KeyResult.KeyHash.- Raw-key fallback in
summarize()--KeyPrefixis read directly.
First release candidate. Phases 0-7 complete; full pre-red-team security audit
performed and all High/Critical findings remediated.
Added
MCP tool surface (16 tools)
- EVM reads (8):
evm_get_chain_id,evm_get_block,evm_get_transaction,
evm_get_transaction_receipt,evm_get_balance,evm_get_code,
evm_get_logs,evm_call_contract - Anchor reads (4):
anchor_info,anchor_get_registry,
anchor_get_registries,anchor_get_records - Anchor writes (3):
anchor_prepare_add_registry,
anchor_prepare_add_record,anchor_prepare_grant_role - Broadcast (1):
evm_send_raw_transaction
All tool inputs validated at the MCP boundary. All outputs normalized into
typed JSON with snake_case field names.
Write architecture (prepare-sign-submit)
- Server constructs complete unsigned transactions but never holds private
keys. - Each
anchor_prepare_*tool returns both:raw_tx(RLP-encoded) for local/headless signers (HSM, Vault, CLI)wallet_tx_request(EIP-1193 hex-quantity payload) for MetaMask /
browser wallets
- Human-in-the-loop write approval via MCP elicitation, configurable
per-client (requiredorauto) and globally (WRITE_APPROVAL_DEFAULT). - See
docs/METAMASK_GUIDE.mdfor the browser-wallet walkthrough.
Authentication and authorization
- Two auth providers, selected by
AUTH_PROVIDER:apikey(default) -- self-managed Bearer keys with per-client identity,
backed by a JSON key store with hot-reload.fusionauth-- OAuth/JWT validation via JWKS;automationrole maps
to auto-approval.
- Per-tool RBAC. Roles (
reader/writer/admin/automation)
gate every tool handler. Backward compatible: when no roles are present,
no enforcement. - Per-client rate limiting. Token-bucket via
MCP_RATE_LIMIT(default
60 req/s) andMCP_RATE_BURST(default 10). Returns HTTP429when
exceeded. - Admin REST API on a separate port (
:8081, requiresADMIN_API_KEY)
for runtime key CRUD without server restarts. Constant-time token
comparison, audit-logged mutations, raw key shown once on creation.
Resilience and operations
- Retry with exponential backoff for transient RPC errors (
RPC_MAX_RETRIES,
RPC_INITIAL_BACKOFF,RPC_MAX_BACKOFF).eth_sendRawTransaction
excluded for idempotency. - Token-bucket rate limit on upstream RPC (
RPC_RATE_LIMIT,
RPC_RATE_BURST). - Circuit breaker on upstream RPC (
CIRCUIT_BREAKER_THRESHOLD,
CIRCUIT_BREAKER_TIMEOUT). - Per-tool request timeouts via context.
- Graceful shutdown on
SIGINT/SIGTERMwith telemetry flush.
Observability
- OpenTelemetry traces and metrics on every MCP tool call and upstream
RPC call. - Configurable trace sampling (
OTEL_TRACE_SAMPLE_RATIO) using
ParentBased(TraceIDRatioBased(...)). - Prometheus
/metricsendpoint on the dedicated metrics port. - Health check endpoints:
/healthz(liveness),/readyz(readiness with
EVM RPC + ABI checks). - Structured
sloglogging with redaction (addresses, URLs, tx data,
private keys). - Per-client identity (
client_id) on every span and audit log entry.
Deployment artifacts
- Dockerfile with digest-pinned base images (
golang:1.26.2-alpineand
gcr.io/distroless/static-debian12),ARG TARGETARCHfor cross-platform
builds, runs as UID 65532 nonroot, read-only filesystem, all caps dropped. - Kubernetes manifests in
deploy/k8s/: Namespace, Deployment, Service,
ServiceMonitor, HPA, ConfigMap, NetworkPolicy, Kustomization. - Helm chart in
deploy/helm/inveniam-mcp-server/with security context
parity. - Grafana dashboard JSON (
deploy/grafana/dashboard.json). - Prometheus alerting rules (
deploy/prometheus/alerts.yaml) with
runbook_urlannotations pointing todocs/RUNBOOK.md.
Security and supply chain
- Pre-red-team security audit performed (
docs/SECURITY_AUDIT.md):
18 findings; 17 remediated (all High/Critical), 1 backlog item (CORS,
Low priority). gosecand 17+golangci-lintlinters in CI.govulncheckruns in CI; no known vulnerabilities.go-licensescheck on every push/PR with explicit allowed-licenses list.- SBOM (CycloneDX JSON) generated by
anchore/sbom-actionon every push to
main. - Cosign keyless signing of compiled binary on every push to
mainvia
Sigstore OIDC. detect-secretspre-commit hook with project baseline.- Dependabot configured for
gomod,docker, andgithub-actions.
Testing
- 271 automated tests across 30 test files in 8 packages.
- Layers: unit tests with mocks, golden tests for response-shape stability,
integration tests against the live Inveniam testnet
(//go:build integration), MCP HTTP end-to-end tests through the
official MCP SDK, k6 load tests, Docker smoke test. - Standard library
testingonly -- no third-party test frameworks. - E2E coverage of approval flows (auto / required / declined / canceled /
no-elicitation), API key auth, FusionAuth JWT validation, per-client
approval overrides, admin API, RBAC enforcement, rate limiting.
Documentation
README.md-- overview, configuration, tool catalog, deployment.docs/DESIGN.md-- architecture, package responsibilities, write flow,
observability, security.docs/IMPLEMENTATION_PLAN.md-- phased plan with completion status.docs/SECURITY_AUDIT.md-- threat model and remediation log.docs/RUNBOOK.md-- operational runbook with alert response procedures.docs/TOOL_REFERENCE.md-- complete schema reference for all 16 tools.docs/METAMASK_GUIDE.md-- browser-wallet quick start.docs/TESTING.md-- testing strategy and latest results.docs/OVERVIEW.md-- capabilities overview.docs/standards/CODING_STANDARDS.md-- contributor coding standards..cursor/rules/-- IDE rules for AI-assisted development.
Tech stack
- Go 1.26.2 (
CGO_ENABLED=0) - MCP Go SDK v1.5.0 (
github.com/modelcontextprotocol/go-sdk) - go-ethereum v1.17.2 (
github.com/ethereum/go-ethereum) - OpenTelemetry SDK v1.43.0
- Resilience:
cenkalti/backoff/v5,sony/gobreaker/v2,
golang.org/x/time/rate - Auth:
MicahParks/keyfunc/v3,golang-jwt/jwt/v5
Target chain
- NVNM Chain (Inveniam L2), Chain ID
58887(0xe607) - MANTRA-secured consumer chain via Interchain Security
- Native currency: mUSD
- Anchor precompile:
0x0000000000000000000000000000000000000A00
Known limitations
- Multi-arch Docker image is not published to any registry yet
(Dockerfile is buildx-ready; needs registry decision -- see
docs/IMPLEMENTATION_PLAN.mdbacklog). - CORS middleware not implemented (Low priority; only relevant for
browser-based MCP clients without a reverse proxy in front). - k6 load test script does not currently send
Authorizationheaders; the
server must be run with no keys configured for load testing, or the
script must be extended. - Self-serve API key request workflow is on the backlog (Medium priority).
Verifying signatures
Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.
# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc0-<os>-<arch>.sha256
# Verify Cosign signature
cosign verify-blob \
--certificate nvnm-mcp-server-v1.0.0-rc0-<os>-<arch>.cert.pem \
--signature nvnm-mcp-server-v1.0.0-rc0-<os>-<arch>.sig \
--certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
nvnm-mcp-server-v1.0.0-rc0-<os>-<arch>