npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mcpc-tech/acp-ai-provider

v0.3.2

Published

[![NPM Version](https://img.shields.io/npm/v/@mcpc-tech/acp-ai-provider)](https://www.npmjs.com/package/@mcpc-tech/acp-ai-provider) [![JSR](https://jsr.io/badges/@mcpc/acp-ai-provider)](https://jsr.io/@mcpc/acp-ai-provider)

Downloads

7,688

Readme

@mcpc/acp-ai-provider

NPM Version JSR

Version Compatibility

  • v0.2.x requires AI SDK v6 (main branch)
  • For AI SDK v5, use the release-v5 branch or install @mcpc-tech/acp-ai-provider@ai-v5

Use ACP (Agent Client Protocol) agents with the AI SDK.

acp-demo

This package bridges ACP agents to the AI SDK. It spawns ACP agents (Claude Code, Gemini, Codex CLI, and more) as child processes and exposes them through the AI SDK's LanguageModelV3/LanguageModelV2 protocol.

Try a full stack web ACP example here

Installation

# npm
npm i @mcpc-tech/acp-ai-provider

# deno
deno add jsr:@mcpc/acp-ai-provider

For AI SDK v5 users:

# npm (v5)
npm i @mcpc-tech/acp-ai-provider@ai-v5

# deno (v5)
deno add jsr:@mcpc/[email protected]

Usage

See all examples

Basic Example

import { createACPProvider } from "@mcpc/acp-ai-provider";
import { generateText } from "ai";
import process from "node:process";

// Create provider for an ACP agent
const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  session: {
    cwd: process.cwd(),
    mcpServers: [],
  },
});

// Use with AI SDK
const result = await generateText({
  model: provider.languageModel(),
  prompt: "Hello, what can you help me with?",
  tools: provider.tools,
});

console.log(result.text);

Streaming Example

import { createACPProvider } from "@mcpc/acp-ai-provider";
import { streamText } from "ai";
import process from "node:process";

const provider = createACPProvider({
  command: "claude-agent-acp",
  args: [],
  session: {
    cwd: process.cwd(),
    mcpServers: [],
  },
});

const { textStream } = streamText({
  model: provider.languageModel(),
  prompt: "Write a simple Hello World program",
  tools: provider.tools,
});

for await (const chunk of textStream) {
  process.stdout.write(chunk);
}

Structured JSON Output

Use AI SDK's Output.object() to get structured JSON from ACP agents. The provider automatically injects JSON schema instructions into the prompt and strips markdown fences from the response if needed.

import { createACPProvider } from "@mcpc/acp-ai-provider";
import { generateText, Output, streamText } from "ai";
import process from "node:process";
import { z } from "zod";

const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  session: { cwd: process.cwd(), mcpServers: [] },
});

const result = await generateText({
  model: provider.languageModel(),
  prompt: "Give me a recipe for chocolate chip cookies.",
  output: Output.object({
    schema: z.object({
      name: z.string(),
      ingredients: z.array(z.object({ item: z.string(), amount: z.string() })),
      steps: z.array(z.string()),
    }),
  }),
});

console.log(result.output); // Typed object matching the schema

This also works with streamText:

const stream = streamText({
  model: provider.languageModel(),
  prompt: "Tell me about Tokyo",
  output: Output.object({
    schema: z.object({
      name: z.string(),
      country: z.string(),
      landmarks: z.array(z.string()),
    }),
  }),
});

const output = await stream.output; // Parsed object

How it works: When Output.object() (or Output.array(), Output.json(), Output.choice()) is used, AI SDK sets responseFormat.type = "json" with an optional JSON Schema. The ACP provider detects this and:

  1. Prepends a structured-output instruction (with the schema) to the prompt
  2. Strips markdown code fences from the response (in both generate and stream modes)
  3. Passes clean JSON text to AI SDK for validation and parsing

Authentication (Lazy by Default)

Authentication is lazy by default: the provider does not authenticate during initialize. If an ACP request fails with an auth-required error, the provider will:

  1. call authenticate(authMethodId)
  2. retry the request once

By default, if authMethodId is not set and initialize.authMethods is available, the provider will use the first method and print a warning.

To explicitly control this flow, set authMethodId:

const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  authMethodId: process.env.AUTH_METHOD_ID,
  session: { cwd: process.cwd(), mcpServers: [] },
});

You can also authenticate manually:

await provider.authenticate(process.env.AUTH_METHOD_ID);

With Tools (MCP Servers)

Tools are defined through MCP (Model Context Protocol) servers, not AI SDK's tools parameter:

const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  session: {
    cwd: process.cwd(),
    mcpServers: [
      {
        type: "stdio",
        name: "filesystem",
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      },
    ],
  },
});

const result = await generateText({
  model: provider.languageModel(),
  prompt: "List files in /tmp",
  tools: provider.tools,
});

Dynamic Host-Side Tools (Experimental)

You can also define AI SDK-style tools that execute on the host side using acpTools():

import { acpTools, createACPProvider } from "@mcpc/acp-ai-provider";
import { streamText, tool } from "ai";
import { z } from "zod";

const provider = createACPProvider({
  command: "claude-agent-acp",
  session: { cwd: process.cwd(), mcpServers: [] },
});

const result = await streamText({
  model: provider.languageModel(),
  prompt: "Please greet Alice",
  // acpTools() registers host-side tools for the agent to call
  tools: acpTools({
    greet: tool({
      description: "Greet a person by name",
      inputSchema: z.object({
        name: z.string().describe("The name of the person to greet"),
      }),
      execute: async ({ name }) => `Hello, ${name}!`,
    }),
  }),
});

Tool Result Format

By default, tools can return simple values like strings or objects:

execute: async ({ name }) => `Hello, ${name}!`,
execute: async () => ({ status: "ok" }),

These are automatically wrapped into the MCP CallToolResult format with a text content block.

However, for tools that return rich content like images or audio, you should return the MCP CallToolResult format directly:

execute: async ({ url }) => {
  const response = await fetch(url);
  const buffer = Buffer.from(await response.arrayBuffer());

  return {
    content: [
      {
        type: "text" as const,
        text: `Fetched image from ${url}`,
      },
      {
        type: "image" as const,
        data: buffer.toString("base64"),
        mimeType: response.headers.get("content-type") || "image/jpeg",
      },
    ],
  };
},

Why? Under the hood, acpTools() are implemented as MCP tools. Simple returns get wrapped as { content: [{ type: "text", text: ... }] }. If you return an object with image or audio blocks without the MCP wrapper, those media blocks get JSON-stringified into a text block and lost. By returning the MCP CallToolResult format explicitly, the media data is preserved and sent to the agent as proper MCP content blocks.

See image-tool-result-example.ts for a complete working example.

How It Works (TCP Socket Callback)

Since ACP agents spawn their own MCP server subprocesses, we use a TCP socket for the runtime to call back to the host for tool execution:

┌─────────────────────────────────────────────────────────┐
│  Host Process                                           │
│    - Starts TCP server (random port)                    │
│    - Passes TCP port via env vars to ACP                │
│                         ▲    │                          │
│                         │    │ TCP (getTools → definitions)
│                         │    │ TCP (callHandler → execute)
│                         │    │                          │
└─────────────────────────┼────┼──────────────────────────┘
                          │    │
┌─────────────────────────┼────┼──────────────────────────┐
│  ACP Agent spawns tool-proxy-runtime                    │
│    - Reads port from ACP_TOOL_PROXY_PORT env            │
│    - Connects and requests tools via `getTools`         │
│    - On MCP tools/call → TCP callHandler → result       │
└─────────────────────────────────────────────────────────┘

Session Management

Key Difference: ACP providers maintain stateful sessions across multiple requests, while streamText itself is stateless. Each streamText call is independent, but the underlying ACP agent process can persist conversation context when properly managed.

Simple Session Persistence (Single Provider)

For simple use cases, use persistSession to keep the same provider alive:

const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  session: { cwd: process.cwd(), mcpServers: [] },
  persistSession: true, // Keep session alive
});

const model = provider.languageModel();
await generateText({ model, prompt: "Hi, my name is Alice" });
await generateText({ model, prompt: "What's my name?" }); // Agent remembers

provider.cleanup(); // Clean up when done

Resume a previous session:

const provider = createACPProvider({
  command: "gemini",
  args: ["--experimental-acp"],
  session: { cwd: process.cwd(), mcpServers: [] },
  existingSessionId: "previous-session-id",
  persistSession: true,
});

Multi-Session Management (Server/API Pattern)

For server applications handling multiple concurrent users, manage sessions in a Map:

interface SessionEntry {
  provider: ReturnType<typeof createACPProvider>;
  createdAt: number;
}

const sessionProviders = new Map<string, SessionEntry>();

// Initialize a new session
async function initSession(agentCommand: string): Promise<string> {
  const provider = createACPProvider({
    command: agentCommand,
    args: [],
    session: { cwd: process.cwd(), mcpServers: [] },
    persistSession: true,
  });

  const session = await provider.initSession();
  const sessionId = session.sessionId;

  sessionProviders.set(sessionId, { provider, createdAt: Date.now() });
  return sessionId;
}

// Use existing session
async function chat(sessionId: string, prompt: string) {
  const entry = sessionProviders.get(sessionId);
  if (!entry) throw new Error("Session not found");

  const { textStream } = streamText({
    model: entry.provider.languageModel(),
    prompt,
    tools: entry.provider.tools,
  });

  // Stream response...
}

// Cleanup session
function cleanupSession(sessionId: string) {
  const entry = sessionProviders.get(sessionId);
  if (entry) {
    entry.provider.cleanup();
    sessionProviders.delete(sessionId);
  }
}

See session-management-example.ts for a complete working example.

Selecting Models and Modes

Some ACP agents support multiple models or modes. Use initSession() to discover and select them (or simply provide an arbitrary value to get an error message listing available options):

const provider = createACPProvider({
  command: "claude-agent-acp",
  args: [],
  session: { cwd: process.cwd(), mcpServers: [] },
  persistSession: true,
});

// Initialize and get available options
const session = await provider.initSession();

// Check available modes (e.g., "default", "acceptEdits", "plan")
console.log(session.modes?.availableModes);

// Check available models (e.g. "default", "opus", "haiku")
console.log(session.models?.availableModels);

// Now use the model
const result = await generateText({
  // You can optionally specify the model ID here
  model: provider.languageModel("opus", "plan"),
  prompt: "...",
});

FAQ

How to stream tool calls

Tools are passed to the AI SDK as provider-defined tools because they are called and executed by the ACP agent (for example, Codex).

So, to stream tool calls, pass the provider tools to the AI SDK:

const result = await generateText({
  model: provider.languageModel(),
  prompt: "List files in /tmp",
  tools: provider.tools,
});

The actual tool name and arguments live inside acp.acp_provider_agent_dynamic_tool's input and follow this structure:

export const providerAgentDynamicToolSchema = z.object({
  toolCallId: z.string().describe("The unique ID of the tool call."),
  toolName: z.string().describe("The name of the tool being called."),
  args: z.record(z.any()).describe("The input arguments for the tool call."),
});

You can import this constant using ACP_PROVIDER_AGENT_DYNAMIC_TOOL_NAME.

Raw stream parts (plan, diffs, terminals)

The provider emits additional data as raw stream parts.

Important: You must set includeRawChunks: true to receive raw stream parts, otherwise they will be filtered out by the AI SDK.

You can handle them directly in the stream:

const { fullStream } = streamText({
  includeRawChunks: true, // Required to receive raw parts
  model: provider.languageModel(),
  prompt: "...",
});

for await (const chunk of fullStream) {
  if (chunk.type === "raw") {
    const data = JSON.parse(chunk.rawValue);

    switch (data.type) {
      case "plan":
        // Plan steps: data.entries
        break;
      case "diff":
        // File changes: data.path, data.oldText, data.newText, data.toolCallId
        break;
      case "terminal":
        // Terminal output: data.terminalId, data.toolCallId
        break;
    }
  }
}

Or use messageMetadata to attach them to messages when streaming to UI:

const result = streamText({
  includeRawChunks: true, // Required to receive raw parts
  model: provider.languageModel(),
  prompt: "...",
});

const response = result.toUIMessageStreamResponse({
  messageMetadata: ({ part }) => {
    // Convert raw parts to metadata for easier UI access
    if (part.type === "raw" && part.rawValue) {
      const data = JSON.parse(part.rawValue as string);
      switch (data.type) {
        case "plan":
          return { plan: data.entries };
        case "diff":
          return { diffs: [data] }; // Accumulate multiple diffs
        case "terminal":
          return { terminals: [data] }; // Accumulate terminal outputs
      }
    }
  },
});

// In your UI component:
// message.metadata?.plan → plan entries
// message.metadata?.diffs → file changes
// message.metadata?.terminals → terminal outputs

Performance Optimization

For the best user experience, we recommend pre-initializing the session with your tools. Benchmarking shows this can reduce the Time to First Token (TTFT) by over 60%.

| Strategy | Connect Time | TTFT (Perceived) | | ------------------- | ------------ | ---------------- | | Standard (Lazy) | N/A | ~7.3s | | Pre-Initialized | ~2.3s | ~2.8s |

// 1. Create provider and tools
const provider = createACPProvider({/* ... */});
const tools = acpTools({/* ... */});

try {
  // 2. Pre-initialize to warm up connection (saves ~5s)
  await provider.initSession(tools);

  // 3. Use in streamText (instant start)
  await streamText({
    model: provider.languageModel("model-id"),
    tools, // Must use same tools instance
    prompt: "Hello",
  });
} finally {
  provider.cleanup();
}

Debugging

Set the ACP_AI_PROVIDER_DEBUG environment variable to enable debug logging:

# Enable debug logging
export ACP_AI_PROVIDER_DEBUG=1

# Run your script
npx tsx example.ts

When enabled, raw ACP messages are logged to a temporary file:

[acp-ai-provider] Agent message log: /tmp/acp-ai-provider-xxx/agent-messages.ndjson

Limitations

  • No token counting — ACP doesn't provide token usage information (it always returns 0).
  • Dynamic tools are experimental — The tools parameter uses TCP callback which adds some complexity.

Related

License

MIT