Browse documentation
Mastra
An MCP client, or a typed tool, in a TypeScript agent.
Through MCP
import { MCPClient } from "@mastra/mcp";
import { Agent } from "@mastra/core/agent";
import { anthropic } from "@ai-sdk/anthropic";
const mcp = new MCPClient({
servers: {
unlob: {
url: new URL("https://api.unlob.com/mcp"),
requestInit: { headers: { "x-api-key": process.env.UNLOB_API_KEY! } },
},
},
});
export const researcher = new Agent({
name: "researcher",
instructions:
"Search with unlob. Use assemble_context for open questions and web_search for " +
"specific ones. Call corroborate before stating anything as established fact.",
model: anthropic("claude-opus-5"),
tools: await mcp.getTools(),
});
getTools() fetches once, at construction. Use getToolsets() instead when the key varies
per request — a multi-tenant app where each user brings their own.
A typed tool
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const webSearch = createTool({
id: "unlob-web-search",
description:
"Search the web for passages. Returns passage text, not links. Each hit carries " +
"independentSources: how many distinct sites assert it.",
inputSchema: z.object({
query: z.string().describe('Supports AND, OR, -exclude, "exact phrase"'),
vertical: z.string().optional(),
limit: z.number().int().min(1).max(20).default(5),
}),
outputSchema: z.object({
incomplete: z.boolean(),
hits: z.array(
z.object({
title: z.string(),
url: z.string(),
text: z.string(),
independentSources: z.number(),
}),
),
}),
execute: async ({ context }) => {
const url = new URL("https://api.unlob.com/search");
url.searchParams.set("q", context.query);
url.searchParams.set("limit", String(context.limit));
// Set here rather than left to the model: drop single-source claims, and fold
// near-duplicates.
url.searchParams.set("min_independent_sources", "2");
url.searchParams.set("collapse", "story");
if (context.vertical) url.searchParams.set("vertical", context.vertical);
const res = await fetch(url, {
headers: { "x-api-key": process.env.UNLOB_API_KEY! },
});
if (!res.ok) throw new Error(`unlob ${res.status}: ${await res.text()}`);
const body = await res.json();
return {
// A short result with no explanation gets reported as "nothing found".
incomplete: body.partial === true,
hits: body.results.map((h: any) => ({
title: h.title,
url: h.url,
text: h.snippet,
independentSources: h.independent_sources ?? 0,
})),
};
},
});
In a workflow
assemble_context fits a workflow step better than a retrieval sub-graph: one call does
search, dedupe, corroborate, rank and pack, and returns items each carrying the reason
they were included.
const gather = createStep({
id: "gather",
execute: async ({ inputData }) => {
const url = new URL("https://api.unlob.com/assemble_context");
url.searchParams.set("q", inputData.question);
url.searchParams.set("budget", "4000");
const pack = await (
await fetch(url, { headers: { "x-api-key": process.env.UNLOB_API_KEY! } })
).json();
return { context: pack.items, tokens: pack.estimated_tokens };
},
});