Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
106 changes: 106 additions & 0 deletions apps/website/content/docs/core-concepts/tools.mdx
Comment thread
FiammaMuscari marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,112 @@ export default async function getData() {
}
```

## Sampling From Tools

Tool handlers can request MCP sampling directly through `extra.sample()`.
Use it when a tool needs a model-generated follow-up, either as a simple
completion or as a tool-enabled flow that can call back into local xmcp tools.
If the connected client does not implement MCP sampling,
`extra.sample()` throws an error instead of silently degrading.

### Sampling behavior in HTTP examples

- With MCP-compatible clients, `extra.sample()` runs the full
`sampling/createMessage` flow.
- With MCPJam, `sampling/createMessage` is not supported today and returns
`-32601 Method not found`, so the HTTP example tools catch that case and
return deterministic fallback responses instead of failing.

Treat a compatible HTTP MCP client as the proof that real sampling works.
Treat MCPJam as proof that the example degrades gracefully when sampling is not
implemented by the client.

```typescript title="src/tools/summarize-with-sample.ts"
import { z } from "zod";
import {
getSampleContext,
type InferSchema,
type ToolExtraArguments,
} from "xmcp";

export const schema = {
topic: z.string().describe("Topic to summarize"),
};

export const outputSchema = {
summary: z.string(),
};

export const metadata = {
name: "summarize_with_sample",
description: "Demonstrates extra.sample() without tool use",
};

export default async function summarizeWithSample(
{ topic }: InferSchema<typeof schema>,
extra: ToolExtraArguments
) {
const result = await extra.sample({
messages: [
{
role: "user",
content: {
type: "text",
text: `Write a concise summary about ${topic}.`,
},
},
],
maxTokens: 300,
});

return getSampleContext(result) || "The client did not return text content.";
}
```

Because `outputSchema` has a single field, xmcp will also mirror that string
into structured output as `structuredContent.summary`.

The runnable HTTP example under
[`examples/http-transport/README.md`](https://github.com/basementstudio/xmcp/blob/canary/examples/http-transport/README.md)
keeps that fallback local to the demo tools so inspector flows can still
complete end-to-end while keeping the behavior explicit.

When you pass `tools`, xmcp resolves the named local tools, forwards their
definitions to the client, executes any returned `tool_use` blocks, and
continues the `tool_use -> tool_result -> continue` loop until the model
finishes or `maxSteps` is reached.

```typescript title="src/tools/research-topic.ts"
const result = await extra.sample({
messages: [
{
role: "user",
content: {
type: "text",
text: `Research ${topic} using the local tools and give me a short answer.`,
},
},
],
tools: ["search_docs"],
toolChoice: { mode: "required" },
maxTokens: 500,
maxSteps: 4,
});
```

Sampling replies can come back as one block or several blocks. When tools are
enabled, those blocks can also include `tool_use` entries. Use
`getSampleContext(result)` when you just want the text to return from your
handler, or `getSampleContentBlocks(result)` when you need the raw blocks.

A runnable HTTP example lives under
[`examples/http-transport/README.md`](https://github.com/basementstudio/xmcp/blob/canary/examples/http-transport/README.md),
with the sampling tools in
[`summarize-with-sample.ts`](https://github.com/basementstudio/xmcp/blob/canary/examples/http-transport/src/tools/summarize-with-sample.ts),
[`research-topic.ts`](https://github.com/basementstudio/xmcp/blob/canary/examples/http-transport/src/tools/research-topic.ts),
and
[`search-docs.ts`](https://github.com/basementstudio/xmcp/blob/canary/examples/http-transport/src/tools/search-docs.ts).

## Troubleshooting

### Tool Loading Errors
Expand Down
68 changes: 68 additions & 0 deletions examples/http-transport/README.md
Comment thread
FiammaMuscari marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# HTTP Transport Example

This example demonstrates `extra.sample()` over Streamable HTTP.

## Sampling behavior in HTTP examples

- With MCP-compatible clients, `extra.sample()` runs the real
`sampling/createMessage` flow and completes the full sampling loop.
- With MCPJam, `sampling/createMessage` is not supported today and returns
`-32601 Method not found`, so the demo tools fall back to deterministic local
responses instead of failing.

Use a compatible HTTP MCP client as the proof that real sampling succeeds over
HTTP.
Use MCPJam only to verify the fallback path and graceful degradation.

## Run the server

From this directory:

```bash
pnpm install
pnpm dev
```

Use the MCP URL printed in the terminal when the server starts.
If the default port is free, that URL will usually be `http://localhost:3000/mcp`.

## Open it in MCPJam

Connect MCPJam to the HTTP server URL printed by `pnpm dev`.

```txt
http://localhost:3000/mcp
```

## Try the sampling tools

Once MCPJam opens:

1. Confirm the server is connected in the `MCP Servers` tab.
2. Open the `Tools` tab and run `summarize_with_sample` with:

```json
{
"topic": "Model Context Protocol"
}
```

With an MCP-compatible client, this exercises the basic `extra.sample()` flow
and returns sampled text.
With MCPJam, it returns a deterministic fallback message instead of failing the
tool run.

3. Run `research_topic` with:

```json
{
"topic": "xmcp sampling helper"
}
```

With an MCP-compatible client, this exercises the local tool loop. The model
asks for `search_docs`, xmcp runs that tool, and then the sampling helper
continues the `tool_use -> tool_result -> continue` flow before returning the
final text.
With MCPJam, it falls back to calling `search_docs` directly so the example
still completes end-to-end over HTTP.
56 changes: 56 additions & 0 deletions examples/http-transport/src/tools/research-topic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { z } from "zod";
import {
getSampleTextContent,
type InferSchema,
type ToolExtraArguments,
} from "xmcp";
import searchDocs from "./search-docs";

export const schema = {
topic: z.string().describe("Topic to research"),
};

export const metadata = {
name: "research_topic",
description: "Demonstrates extra.sample() with local tool execution",
};

export default async function researchTopic(
{ topic }: InferSchema<typeof schema>,
extra: ToolExtraArguments
) {
try {
const result = await extra.sample({
messages: [
{
role: "user",
content: {
type: "text",
text: `Research ${topic} using the local tools and give me a short answer.`,
},
},
],
Comment thread
FiammaMuscari marked this conversation as resolved.
Outdated
tools: ["search_docs"],
toolChoice: { mode: "required" },
maxTokens: 500,
maxSteps: 4,
});

return getSampleTextContent(result) || "No text returned.";
} catch (error) {
const message =
error instanceof Error && error.message ? error.message : String(error);

if (
!message.includes("does not support MCP sampling") &&
!message.includes("sampling/createMessage") &&
!message.includes("Method not found")
) {
throw error;
}
Comment thread
FiammaMuscari marked this conversation as resolved.
Outdated

const docsHit = await searchDocs({ query: topic });

return `Fallback research for "${topic}": the connected MCP client does not implement sampling/createMessage yet, so this demo called the local search_docs tool directly. ${docsHit}`;
Comment thread
FiammaMuscari marked this conversation as resolved.
Outdated
}
}
17 changes: 17 additions & 0 deletions examples/http-transport/src/tools/search-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";
import { type InferSchema } from "xmcp";

export const schema = {
query: z.string().describe("Query to look up in the canned docs"),
};

export const metadata = {
name: "search_docs",
description: "Returns canned xmcp notes for the sampling helper demo",
};

export default async function searchDocs({
query,
}: InferSchema<typeof schema>) {
return `Top hit for "${query}": extra.sample() lets xmcp tool handlers request client-side MCP sampling, optionally expose local tools, and continue the tool_use -> tool_result loop until the model finishes.`;
}
54 changes: 54 additions & 0 deletions examples/http-transport/src/tools/summarize-with-sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from "zod";
import {
getSampleContext,
type InferSchema,
type ToolExtraArguments,
} from "xmcp";

export const schema = {
topic: z.string().describe("Topic to summarize"),
};

export const outputSchema = {
summary: z.string(),
};

export const metadata = {
name: "summarize_with_sample",
description: "Demonstrates extra.sample() without tool use",
};

export default async function summarizeWithSample(
{ topic }: InferSchema<typeof schema>,
extra: ToolExtraArguments
) {
try {
const result = await extra.sample({
messages: [
{
role: "user",
content: {
type: "text",
text: `Write a concise summary about ${topic}.`,
},
},
],
maxTokens: 300,
});

return getSampleContext(result) || "The client did not return text content.";
} catch (error) {
const message =
error instanceof Error && error.message ? error.message : String(error);

if (
!message.includes("does not support MCP sampling") &&
!message.includes("sampling/createMessage") &&
!message.includes("Method not found")
) {
throw error;
}

return `Fallback summary for "${topic}": the connected MCP client does not implement sampling/createMessage yet, so this demo returned a deterministic response instead of model-generated text.`;
}
}
4 changes: 3 additions & 1 deletion packages/xmcp/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const CLIENT_IDENTITY = {

const CLIENT_CAPABILITIES = {
capabilities: {
sampling: {},
sampling: {
tools: {},
},
elicitation: {},
roots: { listChanged: true },
},
Expand Down
7 changes: 7 additions & 0 deletions packages/xmcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type {
ToolSchema,
ToolOutputSchema,
ToolExtraArguments,
SampleRequest,
SampleResult,
InferSchema,
} from "./types/tool";
export type { PromptMetadata } from "./types/prompt";
Expand All @@ -19,6 +21,11 @@ export { apiKeyAuthMiddleware } from "./auth/api-key";
export { jwtAuthMiddleware } from "./auth/jwt";

export { createContext } from "./utils/context";
export {
getSampleContext,
getSampleContentBlocks,
getSampleTextContent,
} from "./utils/sample-result";

export { completable } from "@modelcontextprotocol/sdk/server/completable";

Expand Down
Loading