Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-serve-task-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/client": patch
---

serve() now creates a shared task store and passes it to the agent factory via ServeContext, fixing MCP Tasks protocol (tasks/get) failures over stateless HTTP where each request previously got its own empty task store.
8 changes: 5 additions & 3 deletions docs/guides/BUILD-AN-AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
GetSignalsRequestSchema,
} from '@adcp/client';

function createAgent() {
const server = createTaskCapableServer('My Signals Agent', '1.0.0');
function createAgent({ taskStore }) {
const server = createTaskCapableServer('My Signals Agent', '1.0.0', { taskStore });

server.tool(
'get_signals',
Expand Down Expand Up @@ -193,7 +193,7 @@ Key storyboards for server-side builders:

### HTTP Transport

The `serve()` helper handles HTTP transport setup. Pass it a factory function that returns a configured `McpServer`:
The `serve()` helper handles HTTP transport setup. Pass it a factory function that receives a `ServeContext` and returns a configured `McpServer`:

```typescript
import { serve } from '@adcp/client';
Expand All @@ -203,6 +203,8 @@ serve(createMyAgent, { port: 8080 }); // custom port
serve(createMyAgent, { path: '/v1/mcp' }); // custom path
```

`serve()` creates a shared task store and passes it to your factory on every request via `{ taskStore }`. Pass it through to `createTaskCapableServer()` so MCP Tasks work correctly across stateless HTTP requests.

`serve()` returns the underlying `http.Server` for lifecycle control (e.g., graceful shutdown).

For custom routing or middleware, you can wire the transport manually:
Expand Down
4 changes: 2 additions & 2 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ AdCP is an open protocol for AI agents to buy, manage, and optimize advertising
```typescript
import { createTaskCapableServer, taskToolResponse, serve, GetSignalsRequestSchema } from '@adcp/client';

function createAgent() {
const server = createTaskCapableServer('My Agent', '1.0.0');
function createAgent({ taskStore }) {
const server = createTaskCapableServer('My Agent', '1.0.0', { taskStore });
server.tool('get_signals', 'Discover segments.', GetSignalsRequestSchema.shape, async (args) => {
return taskToolResponse({ signals: [...], sandbox: true }, 'Found segments');
});
Expand Down
5 changes: 3 additions & 2 deletions examples/signals-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { createTaskCapableServer, taskToolResponse, serve, GetSignalsRequestSchema } from '@adcp/client';
import type { GetSignalsResponse } from '@adcp/client';
import type { GetSignalsResponse, ServeContext } from '@adcp/client';

// ---------------------------------------------------------------------------
// Audience segment catalog — typed to match the AdCP signals response schema
Expand Down Expand Up @@ -132,8 +132,9 @@ function querySegments(args: {
// ---------------------------------------------------------------------------
// Server factory
// ---------------------------------------------------------------------------
function createSignalsAgent() {
function createSignalsAgent({ taskStore }: ServeContext) {
const server = createTaskCapableServer('Example Signals Agent', '1.0.0', {
taskStore,
instructions: 'Signals agent providing audience segment discovery via get_signals.',
});

Expand Down
4 changes: 2 additions & 2 deletions scripts/generate-agent-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,8 @@ function generateLlmsTxt(
ln('```typescript');
ln(`import { createTaskCapableServer, taskToolResponse, serve, GetSignalsRequestSchema } from '@adcp/client';`);
ln();
ln(`function createAgent() {`);
ln(` const server = createTaskCapableServer('My Agent', '1.0.0');`);
ln(`function createAgent({ taskStore }) {`);
ln(` const server = createTaskCapableServer('My Agent', '1.0.0', { taskStore });`);
ln(` server.tool('get_signals', 'Discover segments.', GetSignalsRequestSchema.shape, async (args) => {`);
ln(` return taskToolResponse({ signals: [...], sandbox: true }, 'Found segments');`);
ln(` });`);
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ export type {
CreateTaskResult,
GetTaskResult,
Task,
ServeContext,
ServeOptions,
} from './server';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export type {
} from './tasks';

export { serve } from './serve';
export type { ServeOptions } from './serve';
export type { ServeContext, ServeOptions } from './serve';
42 changes: 37 additions & 5 deletions src/lib/server/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* ```typescript
* import { createTaskCapableServer, serve } from '@adcp/client';
*
* function createAgent() {
* const server = createTaskCapableServer('My Agent', '1.0.0');
* function createAgent({ taskStore }) {
* const server = createTaskCapableServer('My Agent', '1.0.0', { taskStore });
* server.tool('get_signals', 'Discover audiences.', schema, handler);
* return server;
* }
Expand All @@ -21,6 +21,23 @@
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createServer, type Server as HttpServer } from 'http';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { TaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';

/**
* Context passed to the agent factory on each request.
*
* Contains shared resources that must survive across stateless HTTP requests,
* such as the task store for MCP Tasks protocol support.
*
* This helper is designed for single-tenant servers (one agent per process).
* For multi-tenant deployments, provide a custom `TaskStore` via `ServeOptions`
* that enforces tenant/session scoping.
*/
export interface ServeContext {
/** Shared task store — use this when creating your McpServer so tasks persist across requests. */
taskStore: TaskStore;
}

export interface ServeOptions {
/** Port to listen on. Defaults to PORT env var or 3001. */
Expand All @@ -29,6 +46,14 @@ export interface ServeOptions {
path?: string;
/** Called when the server starts listening. */
onListening?: (url: string) => void;
/**
* Custom task store. Defaults to a shared InMemoryTaskStore.
*
* The default InMemoryTaskStore only evicts tasks that have a TTL set.
* For long-running servers, always set `ttl` when creating tasks, or
* provide a store with automatic eviction to prevent unbounded memory growth.
*/
taskStore?: TaskStore;
}

/**
Expand All @@ -38,25 +63,32 @@ export interface ServeOptions {
* StreamableHTTPServerTransport, and returns the underlying http.Server
* for lifecycle control.
*
* A shared task store is created once and passed to the factory on every
* request via `ServeContext`. This ensures MCP Tasks (create → poll → result)
* work correctly across stateless HTTP requests.
*
* @param createAgent - Factory function that returns a configured McpServer.
* Called once per request so each gets a fresh server instance (McpServer
* can only be connected once).
* can only be connected once). Receives a `ServeContext` with a shared
* `taskStore` — pass it to `createTaskCapableServer()` so tasks persist.
* @param options - Port, path, and callback configuration.
* @returns The http.Server instance. Use the `onListening` callback or
* listen for the 'listening' event to know when it's ready.
*/
export function serve(createAgent: () => McpServer, options?: ServeOptions): HttpServer {
export function serve(createAgent: (ctx: ServeContext) => McpServer, options?: ServeOptions): HttpServer {
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
if (envPort !== undefined && (Number.isNaN(envPort) || envPort < 0 || envPort > 65535)) {
throw new Error(`Invalid PORT environment variable: "${process.env.PORT}"`);
}
const port = options?.port ?? envPort ?? 3001;
const mountPath = options?.path ?? '/mcp';
const taskStore = options?.taskStore ?? new InMemoryTaskStore();
const ctx: ServeContext = { taskStore };

const httpServer = createServer(async (req, res) => {
const { pathname } = new URL(req.url || '', 'http://localhost');
if (pathname === mountPath || pathname === `${mountPath}/`) {
const agentServer = createAgent();
const agentServer = createAgent(ctx);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
Expand Down
Loading
Loading