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
6 changes: 6 additions & 0 deletions mcp-examples/web-search-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Server configuration (no credentials needed — this is a fully-mock demo)
PORT=3010

# External URL (optional; advertised base URL for remote/HTTPS deployments)
# Leave unset for local dev (defaults to http://localhost:PORT)
# BASE_URL=https://your-external-domain.com
4 changes: 4 additions & 0 deletions mcp-examples/web-search-mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
.env
*.log
24 changes: 24 additions & 0 deletions mcp-examples/web-search-mcp/FLEET-SMOKE-TEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Testing web-search-mcp with LangSmith Fleet

This is a **fully-mock** server: **no auth, no credentials**. Fleet connects to it as a plain remote MCP server (no bearer) and the agent calls the two tools; the point is to capture the agent *buying* Exa / Baselayer access mid-task.

## Steps

1. **Run + expose over HTTPS** (cloud Fleet needs an HTTPS URL):
```bash
yarn install && yarn dev # PORT 3010
ngrok http 3010 # copy the https URL
```
2. **Sanity check** the endpoint returns the two tools:
```bash
curl -s -X POST https://<host>/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
You should see `web_search` and `company_lookup`.
3. **Connect in Fleet** → the agent's **Toolbox → MCP → Add server**: URL `https://<host>/mcp`, **no authentication**.
4. **Run it** — prompt the agent to research a topic (it calls `web_search`), then to get verified company data (it calls `company_lookup`). Each reply ends with a footer showing the buy, e.g. `✓ Exa API access purchased · $7 charged to your card via Nevermined delegation` — capture the chat for the cookbook (image1 / image5).

## Making it real (production)

This mock stands in for the sellers. The real buyer flow mints an x402 **card-delegation** token via `payments.x402.getX402AccessToken(EXA_PLAN_ID, undefined, { scheme: "nvm:card-delegation", delegationConfig })` and POSTs it (`payment-signature` header) to Exa's `admin-api.exa.ai/team-management/nevermined/purchase-key` — **Exa** verifies + settles and returns the API key; then `api.exa.ai/search` with `x-api-key`. Baselayer follows the same pattern. See the README's "Making it real (production)" section.
58 changes: 58 additions & 0 deletions mcp-examples/web-search-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Web Search MCP — fully-mock buyer-side demo

A **fully-mock** MCP server for the "Agents That Pay" cookbook. It exposes two tools over streamable-HTTP and returns canned results — **no credentials, no auth, no payments**. It exists so a **LangChain Fleet** agent can be shown *buying* from external sellers (Exa / Baselayer) mid-task, for screenshots.

> In the real flow the **sellers** (Exa, Baselayer) verify + settle the payment; this server is **not** a paywall. It only stands in for their responses so the buying moment is visible in the Fleet chat.

## Tools

| Tool | Args | Returns |
|---|---|---|
| `web_search` | `{ query: string }` | Mock Exa-style results (title, url, snippet) + a "paid Exa via delegation" footer |
| `company_lookup` | `{ name: string }` | Mock Baselayer-style verified company data + a "paid Baselayer via delegation" footer |

## Run (zero setup — no creds)

```bash
yarn install
yarn dev # tsx src/main.ts (default PORT 3010)
```

- MCP endpoint: `POST http://localhost:3010/mcp` (streamable-HTTP, **no auth** — Fleet connects with no bearer).
- Health: `GET http://localhost:3010/health`.

Quick check:

```bash
curl -s -X POST http://localhost:3010/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

## Test with LangSmith Fleet

See [`FLEET-SMOKE-TEST.md`](./FLEET-SMOKE-TEST.md) — expose the server over HTTPS, add it to a Fleet agent as a remote MCP server (no auth), prompt the agent, and capture the chat.

## Making it real (production)

To turn this into a real buyer, each tool would perform the actual purchase against the seller (the seller verifies + settles). For **Exa** (`web_search`), mint an x402 **card-delegation** token and POST it to Exa, which returns an API key:

```ts
const { accessToken } = await payments.x402.getX402AccessToken(
EXA_PLAN_ID,
undefined,
{
scheme: "nvm:card-delegation",
delegationConfig: { providerPaymentMethodId, spendingLimitCents: 700, durationSecs: 3600 },
},
);
const { apiKey } = await (
await fetch("https://admin-api.exa.ai/team-management/nevermined/purchase-key", {
method: "POST",
headers: { "payment-signature": accessToken },
})
).json();
// then search: POST https://api.exa.ai/search with header x-api-key: <apiKey>
```

(See Exa's docs: `exa.ai/docs/integrations/nevermined.md` — a $7 purchase provisions or tops up the key.) **Baselayer** (`company_lookup`) follows the same pattern against its own agent-payment endpoint. The delegation — an enrolled card + spend caps — is authorized once via the Nevermined embed; the agent then buys inside those caps with no human in the loop.
22 changes: 22 additions & 0 deletions mcp-examples/web-search-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "web-search-mcp-tutorial",
"version": "0.1.0",
"type": "module",
"license": "MIT",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "tsx src/main.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"dotenv": "^17.2.1",
"express": "^5.0.1",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
}
}
143 changes: 143 additions & 0 deletions mcp-examples/web-search-mcp/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Web Search MCP — fully-mock, buyer-side cookbook demo.
*
* A plain MCP server (raw @modelcontextprotocol/sdk) exposing two mock tools over
* streamable-HTTP at POST /mcp. NO auth, NO payments, NO credentials — it boots
* with zero env and lets a LangChain Fleet agent be shown BUYING from external
* sellers (Exa / Baselayer) mid-task. The sellers verify + settle; this server
* only stands in for their responses so the buying flow can be captured.
*
* See "Making it real (production)" in README.md for the actual buyer flow
* (mint an x402 card-delegation token -> Exa purchase-key endpoint).
*/

import "dotenv/config";
import express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { webSearch, companyLookup } from "./services/search.service.js";

const PORT = parseInt(process.env.PORT || "3010", 10);
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;

/**
* Chat-visible footers. These represent the agent PAYING THE EXTERNAL SELLER
* (Exa / Baselayer) via a Nevermined card delegation — the seller is the payee,
* the delegation is the rail. Representative wording (this is a mock; nothing is
* actually charged here).
*/
const WEB_SEARCH_FOOTER =
"\n\n———\n✓ Exa API access purchased · $7 charged to your card via Nevermined delegation";
const COMPANY_LOOKUP_FOOTER =
"\n\n———\n✓ Baselayer verified-data access · paid via Nevermined card delegation";

/** Build a fresh MCP server with the two mock tools (one per request; stateless). */
function buildServer(): McpServer {
const server = new McpServer(
{ name: "web-search-mcp", version: "0.1.0" },
{ capabilities: { tools: {} } }
);

server.registerTool(
"web_search",
{
title: "Web search",
description: "Search the web for recent, relevant results (Exa-style).",
inputSchema: {
query: z.string().min(2).max(300).describe("The web search query"),
},
},
async ({ query }) => {
const results = webSearch(query);
const text =
`Top results for "${query}":\n\n` +
results
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`)
.join("\n\n") +
WEB_SEARCH_FOOTER;
return { content: [{ type: "text" as const, text }] };
}
);

server.registerTool(
"company_lookup",
{
title: "Verified company data",
description:
"Look up verified company data — registration, officers, and standing (Baselayer-style).",
inputSchema: {
name: z.string().min(2).max(160).describe("Company name to look up"),
},
},
async ({ name }) => {
const company = companyLookup(name);
const text =
`Verified data for ${company.legalName}:\n` +
`- Registration: ${company.registrationNumber} (${company.jurisdiction})\n` +
`- Status: ${company.status}\n` +
`- Incorporated: ${company.incorporationDate}\n` +
`- Officers: ${company.officers.join(", ")}` +
COMPANY_LOOKUP_FOOTER;
return { content: [{ type: "text" as const, text }] };
}
);

return server;
}

const app = express();
app.use(express.json());

app.get("/health", (_req, res) => {
res.json({
status: "ok",
server: "web-search-mcp",
tools: ["web_search", "company_lookup"],
});
});

// Streamable-HTTP MCP endpoint. Stateless: a fresh server + transport per
// request, and NO auth gate — Fleet connects with no bearer token.
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
void transport.close();
void server.close();
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (err) {
console.error("[mcp] request error:", err);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});

// Stateless server: no SSE stream / session to GET or DELETE.
app.get("/mcp", (_req, res) => {
res.status(405).json({ error: "Method Not Allowed (stateless server)" });
});
app.delete("/mcp", (_req, res) => {
res.status(405).json({ error: "Method Not Allowed (stateless server)" });
});

app.listen(PORT, () => {
console.log(`
🔎 Web Search MCP — fully-mock buyer-side demo (no auth, no creds)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📡 MCP Endpoint: ${BASE_URL}/mcp (POST, streamable-HTTP)
🏥 Health Check: ${BASE_URL}/health
🛠️ Tools: web_search, company_lookup (mock results)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);
});
64 changes: 64 additions & 0 deletions mcp-examples/web-search-mcp/src/services/search.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Mock data for the web-search demo MCP.
*
* Demo/sandbox only: these functions return deterministic MOCK results with no
* external API calls and no API keys. They exist so a LangChain Fleet agent can
* be shown buying `web_search` / `company_lookup` mid-task against a Nevermined
* card delegation. They are NOT the production Exa / Baselayer integrations.
*/

export interface SearchResult {
title: string;
url: string;
snippet: string;
}

export interface CompanyRecord {
legalName: string;
registrationNumber: string;
jurisdiction: string;
status: string;
incorporationDate: string;
officers: string[];
}

/** Deterministic mock web-search results (Exa-style). */
export function webSearch(query: string): SearchResult[] {
const q = query.trim();
return [
{
title: `${q} — overview and recent coverage`,
url: "https://example.com/overview",
snippet: `A concise overview of "${q}", summarizing recent developments and primary sources.`,
},
{
title: `Analysis: what "${q}" means in 2026`,
url: "https://example.org/analysis",
snippet: `Independent analysis of "${q}" with context, key figures, and expert commentary.`,
},
{
title: `${q} — reference documentation`,
url: "https://docs.example.net/reference",
snippet: `Reference material and primary documentation relevant to "${q}".`,
},
];
}

/** Deterministic mock verified-company data (Baselayer-style). */
export function companyLookup(name: string): CompanyRecord {
const clean = name.trim();
return {
legalName: `${clean} Inc.`,
registrationNumber: "REG-" + stableHash(clean),
jurisdiction: "Delaware, US",
status: "Active / Good Standing",
incorporationDate: "2021-03-14",
officers: ["Jane Doe (CEO)", "John Smith (CFO)", "Alex Roe (Secretary)"],
};
}

function stableHash(s: string): string {
let h = 0;
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
return String(h).padStart(8, "0").slice(0, 8);
}
19 changes: 19 additions & 0 deletions mcp-examples/web-search-mcp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading