mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-15 12:30:12 +00:00
The unified fetch tool surfaced HTTP 402 + X-Payment plumbing in PR #5 (TypeScript). The Go HTTP layer mounted in routes.go (HIP-0106) did not propagate the same envelope: a 402 payment-required tool result was returned as an opaque HTTP 200 carrying an in-band JSON marker, and the incoming X-Payment header was never forwarded into the tool params, so agents could not replay a payment quote via the Go surface. This change wires the same two pieces through routes.go: - Request path: X-Payment header is plumbed into the tool params under the payment key before invoker dispatch. Caller-supplied payment objects (the structured shape) are preserved unchanged. - Response path: a tool result envelope of the shape {"status":402,"payment_required":{...}} is mapped to HTTP 402 with WWW-Authenticate: x402 on the wire. The body is forwarded verbatim so the caller still sees the accepts[] payment requirements. Both handleCallTool (POST /v1/mcp/tools/:name) and handleFetch (POST /v1/mcp/fetch shortcut) get the same treatment, so the agent path is identical whether the model talks to the generic tool route or the fetch shortcut. No public type or interface change. The 200 happy path remains unchanged when no X-Payment header is sent and the tool result is not a 402 envelope. Tests ----- go test ./... -run "TestCallTool|TestFetchShortcut|TestMount" -count=1 -v 12 PASS: - 5 existing TestMount* (no regression) - TestCallTool_XPaymentHeaderPlumbedIntoToolParams - TestCallTool_XPaymentMissing_NoOp - TestCallTool_XPaymentDoesNotOverwriteExisting - TestCallTool_PaymentRequiredEnvelopeMappedTo402 - TestCallTool_RegularEnvelopeStays200 - TestCallTool_StatusFieldAloneIsNotEnough - TestFetchShortcut_XPaymentPlumbedAndPropagated Note: hanzomcp/ CGO link failure (missing prebuilt rust lib hanzo_mcp) is environmental, present on main, and unrelated to this change. The pure-Go route layer this PR modifies builds and tests cleanly.
48 lines
1.5 KiB
Go
48 lines
1.5 KiB
Go
package mcp
|
|
|
|
import "encoding/json"
|
|
|
|
// injectPayment merges the X-Payment header value into the JSON params body
|
|
// under the "payment" key so the unified fetch tool can replay the payment
|
|
// per x402. If the body is empty or not a JSON object, returns a minimal
|
|
// {"payment": v} payload. If the caller already supplied a payment field
|
|
// (e.g. a structured payment object), it is preserved unchanged. Returns
|
|
// body verbatim when v is empty.
|
|
func injectPayment(body []byte, v string) []byte {
|
|
if v == "" {
|
|
return body
|
|
}
|
|
var m map[string]any
|
|
if len(body) == 0 || json.Unmarshal(body, &m) != nil {
|
|
out, _ := json.Marshal(map[string]any{"payment": v})
|
|
return out
|
|
}
|
|
if _, exists := m["payment"]; exists {
|
|
out, _ := json.Marshal(m)
|
|
return out
|
|
}
|
|
m["payment"] = v
|
|
out, _ := json.Marshal(m)
|
|
return out
|
|
}
|
|
|
|
// paymentRequiredFromEnvelope returns true iff raw is a JSON object emitted
|
|
// by the unified fetch tool to surface an x402 402 Payment Required
|
|
// response, of the shape:
|
|
//
|
|
// { "status": 402, "payment_required": { ... } }
|
|
//
|
|
// The HTTP layer maps this to a real 402 on the wire (with
|
|
// WWW-Authenticate: x402) so downstream agents see a Payment-Required
|
|
// response instead of an opaque 200 carrying an in-band marker.
|
|
func paymentRequiredFromEnvelope(raw []byte) bool {
|
|
var env struct {
|
|
Status int `json:"status"`
|
|
PaymentRequired json.RawMessage `json:"payment_required"`
|
|
}
|
|
if err := json.Unmarshal(raw, &env); err != nil {
|
|
return false
|
|
}
|
|
return env.Status == 402 && len(env.PaymentRequired) > 0
|
|
}
|