Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mcp-examples/weather-mcp/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Credits can be:

### Testing Without Auth

The MCP Inspector (`yarn inspector`) doesn't send auth headers. For testing with authentication, create a client script using `payments.agents.getAgentAccessToken()`.
The MCP Inspector (`yarn inspector`) doesn't send auth headers. For testing with authentication, create a client script that mints an x402 access token with `payments.x402.getX402AccessToken(planId, agentId, { delegationConfig: { delegationId } })` — create the delegation first via `payments.delegation.createDelegation(...)`. See [`FLEET-SMOKE-TEST.md`](./FLEET-SMOKE-TEST.md) for a full runnable flow.

## Dependencies

Expand Down
74 changes: 74 additions & 0 deletions mcp-examples/weather-mcp/FLEET-SMOKE-TEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Testing weather-mcp with LangSmith Fleet (static bearer header)

[LangSmith Fleet](https://docs.langchain.com/langsmith/fleet) is LangChain's no-code agent builder. It can connect to a **remote MCP server by URL** and attach a **static `Authorization` header** to every request. This runbook proves, with the least setup, that a Fleet agent can pay this Nevermined-gated MCP server through that static-header path.

> **What this proves:** a Fleet agent pays a Nevermined-gated MCP tool with an operator-provisioned, spend-capped token in a static header.
> **What it does not prove:** per-user, agent-driven delegation setup (the card-enroll popup) — that's a separate, OAuth-based flow.

## How the payment travels

This server implements the **x402 v2 MCP transport**: it prefers the in-band payment payload in `_meta["x402/payment"]`, and **falls back to the `Authorization: Bearer <token>` header** when `_meta` is absent (a one-time deprecation warning, not an error). Fleet injects static headers but not per-call `_meta`, so the smoke test rides that header fallback.

> ⚠️ The header fallback is **deprecated**. For a Fleet-backed deployment, pin an exact SDK version (this tutorial uses `@nevermined-io/payments@^1.10.0`) and track the fallback's removal. A first-class OAuth path is the durable integration.

## Prerequisites

- A **sandbox NVM API key** (subscriber) — its account auto-has an `erc4337` smart-account wallet (no card needed). The key prefix now selects the environment (`sandbox:…`), so the `environment` option is no longer required.
- `node` + `yarn`, and **`ngrok`** (Fleet needs an HTTPS URL).
- A Fleet workspace with the **"MCP Server Create"** permission.
- Optional `OPENAI_API_KEY` for richer forecasts.

## Steps

### 1. Install (already on the latest SDK)
```bash
cd mcp-examples/weather-mcp
yarn install # resolves @nevermined-io/payments@1.10.0
```

### 2. Register a plan + agent (sandbox, crypto, fixed-credits)
Use the SDK's `payments.agents.registerAgentAndPlan(...)` (a crypto ERC-20 price config + a fixed-credits config) to get an `{ agentId, planId }`. The `nvm-deploy-e2e` skill in the `nevermined-io/nvm-monorepo` repo (`register-test.ts`) is a ready-made script for this.

### 3. Run the server + expose it over HTTPS
```bash
# .env: NVM_API_KEY=sandbox:... NVM_AGENT_ID=<agentId> PORT=3002 [OPENAI_API_KEY=...]
yarn dev # tsx src/main.ts (use `yarn dev`, not `yarn start`)
# second terminal:
ngrok http 3002 # copy the https URL
```
Set `BASE_URL=https://<ngrok>` in `.env` and restart — `BASE_URL` is what the server's OAuth metadata advertises. Transport is streamable-HTTP at `POST /mcp`.

### 4. Mint a token (crypto delegation — no card)
```ts
const { delegationId } = await payments.delegation.createDelegation({
provider: "erc4337", spendingLimitCents: 10000, durationSecs: 604800, currency: "usdc",
});
const { accessToken } = await payments.x402.getX402AccessToken(
planId, agentId, { delegationConfig: { delegationId } },
);
```
(`getX402AccessToken` **requires** a `delegationConfig`; there is no delegation-free mint. `erc4337` is headless — no popup.)

### 5. Gating check — confirm the header path works *before* touching Fleet
```bash
curl -X POST https://<ngrok>/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <accessToken>" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"weather.today","arguments":{"city":"London"}}}'
```
Expect a forecast, a `_meta["x402/payment-response"]` settlement receipt, and a one-time `[x402] … falling back to the Authorization header` warning in the server log. **If the weather comes back, the static-header contract Fleet needs is proven.**

### 6. Wire it into Fleet
Add a **remote MCP server**: URL = `https://<ngrok>/mcp`, Auth = Header `Authorization` = `Bearer <accessToken>`. Then chat *"what's the weather in London?"* — the agent calls the tool and the server charges against the delegation.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `401 … Authorization header required` | The header must be on **every** call including discovery (`initialize`/`tools/list`). Ensure Fleet attaches it to both. |
| `getAgentAccessToken is not a function` | Removed method — use `payments.x402.getX402AccessToken(planId, agentId, { delegationConfig })`. |
| `delegationConfig is required …` | Pass `delegationConfig: { delegationId }` (create the delegation first). |
| `Invalid NVM API Key` at startup | A real sandbox key is required; the server constructs the Payments client on boot. |
| Header path breaks after an SDK upgrade | The deprecated header fallback may have been removed — pin the SDK version, or move to the OAuth path. |
| `[DEPRECATED] The 'environment' option …` | 1.10.0 derives the environment from the API-key prefix; drop the `environment` option. |
19 changes: 15 additions & 4 deletions mcp-examples/weather-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,23 @@ import { Payments } from "@nevermined-io/payments";

const payments = Payments.getInstance({
nvmApiKey: process.env.NVM_API_KEY!,
environment: "sandbox",
});

// agentId is optional under the plan-centric model — the plan id is all you need
const { accessToken } = await payments.agents.getAgentAccessToken(
process.env.NVM_PLAN_ID!
// 1) Create a delegation (a spending mandate). `erc4337` = crypto, headless, no card.
// For card payments, create the delegation via the Nevermined embed flow and reuse its `delegationId`.
const { delegationId } = await payments.delegation.createDelegation({
provider: "erc4337",
spendingLimitCents: 10000,
durationSecs: 604800,
currency: "usdc",
});

// 2) Mint the x402 access token against the plan.
// agentId is optional under the plan-centric model — the plan id is all you need.
const { accessToken } = await payments.x402.getX402AccessToken(
process.env.NVM_PLAN_ID!,
undefined,
{ delegationConfig: { delegationId } }
);
```

Expand Down
19 changes: 15 additions & 4 deletions mcp-examples/weather-mcp/RUN.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,23 @@ import { Payments } from "@nevermined-io/payments";

const payments = Payments.getInstance({
nvmApiKey: process.env.NVM_API_KEY!,
environment: "sandbox",
});

// agentId is optional under the plan-centric model — the plan id is all you need
const { accessToken } = await payments.agents.getAgentAccessToken(
process.env.NVM_PLAN_ID!
// 1) Create a delegation (a spending mandate). `erc4337` = crypto, headless, no card.
// For card payments, create the delegation via the Nevermined embed flow and reuse its `delegationId`.
const { delegationId } = await payments.delegation.createDelegation({
provider: "erc4337",
spendingLimitCents: 10000,
durationSecs: 604800,
currency: "usdc",
});

// 2) Mint the x402 access token against the plan.
// agentId is optional under the plan-centric model — the plan id is all you need.
const { accessToken } = await payments.x402.getX402AccessToken(
process.env.NVM_PLAN_ID!,
undefined,
{ delegationConfig: { delegationId } }
);

console.log("Access Token:", accessToken);
Expand Down
2 changes: 1 addition & 1 deletion mcp-examples/weather-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@nevermined-io/payments": "^1.9.0",
"@nevermined-io/payments": "^1.10.0",
"dotenv": "^17.2.1",
"express": "^5.0.1",
"openai": "^6.15.0",
Expand Down
8 changes: 4 additions & 4 deletions mcp-examples/weather-mcp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@
zod "^3.25 || ^4.0"
zod-to-json-schema "^3.25.0"

"@nevermined-io/payments@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@nevermined-io/payments/-/payments-1.9.0.tgz#01984f0a2bec136588ff1f27c0d7425f9dc57746"
integrity sha512-POjzDfovZj1DmgtNlgUusF2qGaHzTTkNf0xExxAjo7ZyT72oyIKOanFxtP6pCeg766QThNnzO3ovcVfN7UkyRw==
"@nevermined-io/payments@^1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@nevermined-io/payments/-/payments-1.10.0.tgz#08ad29f4be186c2b2a7a85680f6ee1463b5a4916"
integrity sha512-9VuonZSWhnUrSnOAYQVkEpSJy3peLr0ES2rFAv0/pCnpNh0coEg09Q+QKmpH1A3KL55SUzb7vp62xzZ8RXvWjg==
dependencies:
"@a2a-js/sdk" "^0.3.13"
"@helicone/helpers" "^1.6.0"
Expand Down