Skip to content

MCP evolver_asset_publish can never submit a valid bundle: single-asset body vs the hub's Gene+Capsule requirement, and gep-sdk capsule.schema.json forbids the required validation field #638

Open

Description

@SparkEric

Summary

The MCP publish path (evolver_asset_publish) cannot succeed for any hub-compliant bundle, and it reports success while the asset never reaches the hub. Two independent defects combine, and the receipt hides both.

All three findings below were reproduced on 2026-10-11 against evomap.ai with a single asset submit.

Environment

  • @evomap/evolver 2.0.42 (global npm install, Windows 11 x64)
  • @evomap/evolver-mcp (bundled) -> dist/proxyClient.js
  • @evomap/gep-sdk 1.14.0 -> schemas/capsule.schema.json, schemas/gene.schema.json
  • proxy: running, hub_auth_status: ok, node node_cd8a0948549519f6, hub_mode: public

Defect 1: the MCP tool is structurally limited to one asset, but the hub requires a pair

The tool schema accepts exactly one asset object:

{ "type": "object", "required": ["asset"], "properties": { "asset": { "type": "object" } } }

proxyClient.js then hardcodes the single-asset body (line ~174):

submitAsset(asset) {
    // MCP publishing remains durable and outage-tolerant; the bare route is reserved for V1 synchronous callers.
    return this.call('POST', '/asset/submit?mode=async', this.modeBoundBody({
        assets: [asset],
        request_id: randomUUID(),
    }));
}

The hub's publish contract (GET /a2a/skill?topic=publish) requires payload.assets to contain a Gene and a Capsule, so every MCP submit is a 400 invalid_request. The same file already has a submitAssetBundle(bundle) helper that posts to /asset/submit, but it is not exposed through MCP.

Defect 2: capsule.schema.json forbids the field the hub requires

@evomap/gep-sdk 1.14.0 schemas/capsule.schema.json:

"required": ["type", "schema_version", "id", "trigger", "gene", "summary",
             "confidence", "blast_radius", "outcome", "asset_id"],
"additionalProperties": false

There is no validation property at all, and additionalProperties: false rejects it. schemaGate.js therefore reports Capsule: 多余字段(additionalProperties:false 会 reject): validation for a hub-compliant Capsule.

The hub spec (GET /a2a/skill?topic=structure) says the opposite:

| validation | Yes | Array of validation commands -- same sandbox rules as Gene.validation: node/npm/npx only, min 1 command, each min 10 chars, self-contained |

Because both sides set additionalProperties: false, no single Capsule can satisfy the local wire schema and the hub spec at the same time. The same divergence exists on the Gene side, where gene.schema.json additionally requires id, constraints, schema_version and validation, none of which the hub lists as required (schema_version is explicitly "No", validation "Recommended").

A side effect: evolver_gep_build returns wire_valid: false with Gene: 缺 required 字段: constraints / 缺 required 字段: asset_id even for input that it just built an asset_id for, because the gate validates the pre-build input.

Defect 3: the receipt is false green

Submitting one valid Gene through the MCP tool returns:

{ "status": "pending", "publish_status": "pending", "queued": true, "stored": true }

The proxy's own mailbox (~/.evolver/proxy/mailbox.db, WAL mode) records:

type: asset_submit | status: failed | attempts: 1 | dlq: 1
lastError: publish invalid_request: hub 400

and evolver_asset_fetch for that asset_id returns null. At the time of writing the mailbox holds 5 messages, all status: failed, all with the same hub 400. Note that the rows live in the -wal file, so a plain copy of mailbox.db alone shows an empty messages table.

There is no signal in the receipt that lets a caller distinguish "queued for real" from "will fail forever".

Reproduction

npm i -g @evomap/evolver
# start the proxy, then from any MCP client:
evolver_gep_build({ asset: <a valid Gene with validation commands> })
evolver_asset_publish({ asset: <that Gene> })   # -> pending / queued: true / stored: true
# then:
sqlite3 ~/.evolver/proxy/mailbox.db "SELECT status, dlq, lastError FROM messages;"
evolver_asset_fetch({ asset_id: <the id> })     # -> null

The hub itself accepts the bundle

To confirm this is client-side, the same content published directly works:

POST https://evomap.ai/a2a/publish
Authorization: Bearer <node-scoped secret from the proxy's mailbox kv>
Idempotency-Key: <uuid>
{ "protocol": "gep-a2a", "protocol_version": "1.0.0", "message_type": "publish",
  "message_id": "...", "sender_id": "node_...", "timestamp": "...",
  "payload": { "assets": [Gene, Capsule, EvolutionEvent] } }

Response: 200, decision: accept, reason: auto_promoted, bundle_id: bundle_21f098ad2cecb001. The Gene (sha256:7710499dc7da20f2...) is readable through GET /a2a/fetch with status: promoted, gdi_score: 37.8, source_node_id matching the node. Note that a user-level ek_ API key is rejected on this endpoint with auth_scope_mismatch; only the node-scoped secret works.

Suggested fixes

  1. Expose the pair through MCP, for example evolver_asset_publish_bundle({ assets: [...] }), or let evolver_asset_publish accept assets as an array while keeping asset for the single case.
  2. Add validation to capsule.schema.json (required, min 1 command, node/npm/npx) and drop the Gene fields the hub does not define, so the local wire schema and the hub spec are compatible instead of mutually exclusive. Better, generate the local schemas from the same source the hub serves at /a2a/skill?topic=structure.
  3. Stop reporting queued: true / stored: true before the hub accepts. Either return the hub's decision, or expose the queue state so a caller can tell a real queue entry from a permanent failure, and surface dlq / lastError instead of stored: true.

Scope note

This is reported against EvoMap/evolver because proxyClient.js, the MCP tool surface and the bundled @evomap/gep-sdk schemas all ship here. Related but distinct: #620 (publish returns success but assets never become visible) and #621 (moved to evolver-codex-plugin).

Activity

  1. Paraphern commented on Oct 22, 2026

    @Paraphern

    The worst part of this report is that the publish path reports success while the asset never reaches the hub — a lying receipt is more damaging than a failing one, because downstream automation keys on the success signal. Two mechanical guards: (a) validate the publish body against the hub's published schema before submit — the single-asset vs bundle mismatch becomes a client-side validation error, not a hub rejection; (b) tie the receipt to the hub's actual state — publish returns an artifact id, a follow-up call confirms it's addressable, and only then does the receipt say success. Receipts that assert rather than verify are a class we care about a lot — ours are hash-verifiable and signed for exactly this reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions