> ## Documentation Index
> Fetch the complete documentation index at: https://docs.os.meetluno.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP and A2A

> Expose a project's agents and workflows to MCP clients and A2A agents, call remote A2A agents as tools, and run ACP coding agents in the sandbox.

Every deployed project serves two agent protocols next to its HTTP API, behind the same auth: send an organization API key (or a platform session) as `Authorization: Bearer <org-api-key>`. Both run agents and workflows, so the key needs the `run` permission on the project; a read-scoped key is refused.

| Protocol | URL | What it exposes |
| - | - | - |
| **MCP** (Streamable HTTP) | `/api/projects/<project-id>/mcp` | One tool per agent and per workflow |
| **A2A** 0.3 agent card | `/api/projects/<project-id>/.well-known/<agent>/agent-card.json` | Discovery for one agent |
| **A2A** 0.3 JSON-RPC | `/api/projects/<project-id>/a2a/<agent>` | `message/send`, `tasks/get` |

## MCP server

Point any MCP client (Claude, Cursor, an MCP SDK) at the project's `/mcp` URL with the bearer header. The server lists:

* `ask_<agent>` for every agent. Input `{ message, sessionId? }`. Returns the agent's reply and a `sessionId`; pass it back to continue the same conversation. A `sessionId` that belongs to another user is refused, like on the REST route.
* `workflow_<slug>` for every workflow, with the workflow's input as the tool's JSON Schema. Returns the workflow output.

Each call waits up to about 55 seconds. A longer run returns its `runId` (workflows) or `sessionId` (agents) with the route to read the result later: `GET /workflows/<slug>/runs/<runId>` or `GET /agents/<agent>/sessions/<sessionId>`.

When an agent pauses on a tool that needs [human approval](https://docs.os.meetluno.ai/learn/agents/build-agents.md#human-approval), the tool answers with the pending call and its `approvalId` instead of a reply. Approve or deny it in the web app, the CLI (`luno agents approvals`), or `POST /agents/<agent>/sessions/<sessionId>/approvals/<approvalId>`; the agent then finishes on its own.

Actions are not exposed directly. To publish one, wrap it in a workflow.

```json title="MCP client config" theme={null}
{
  "mcpServers": {
    "my-project": {
      "url": "https://<your-project>/api/projects/<project-id>/mcp",
      "headers": { "Authorization": "Bearer <org-api-key>" }
    }
  }
}
```

## A2A server

Each agent has an [A2A](https://a2a-protocol.org) agent card. Its `url` is the agent's JSON-RPC endpoint.

```bash theme={null}
curl https://<your-project>/api/projects/<project-id>/.well-known/support/agent-card.json \
  -H "Authorization: Bearer <org-api-key>"
```

* `message/send` prompts the agent. The task's `contextId` is the agent session: send it back to continue the conversation. Another user's `contextId` is refused, and `tasks/get` does not find tasks in sessions you don't own (project admins excepted). The call waits like the MCP tools; set `configuration.blocking: false` to return at once.
* `tasks/get` returns the task's state: `working`, `input-required` (paused for approval), `completed` (the reply is in `artifacts`), `failed`, or `canceled`.

Streaming, push notifications, and `tasks/cancel` are not supported.

## Call a remote A2A agent

`defineA2AAgent` turns a remote A2A agent into a tool for `defineAgent({ tools })`. Pass the agent card URL, or a base URL that serves `/.well-known/agent-card.json`.

```ts title="src/agents/support.ts" theme={null}
import { defineA2AAgent, defineAgent } from "@meetluno/luno/agent";

const weather = defineA2AAgent({
  name: "weather-agent",
  description: "Current weather and forecasts for any city.",
  url: "https://weather.example.com/.well-known/weather/agent-card.json",
  headers: () => ({ Authorization: `Bearer ${process.env.WEATHER_AGENT_TOKEN}` }),
});

export default defineAgent({
  slug: "support",
  name: "Support",
  description: "Answers customer questions",
  systemPrompt: "Help customers. Ask the weather agent about weather.",
  model: "anthropic/claude-sonnet-4.6",
  tools: [weather],
});
```

The tool sends `message/send` and, while the remote task is still working, polls `tasks/get` for up to `timeoutMs` (5 minutes by default). Its result includes the remote `contextId`, which the model passes back to continue the remote conversation.

## Use a coding agent (ACP)

`defineAcpAgent` turns a coding agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (Claude Code, Codex, OpenCode, Gemini CLI…) into a tool. The process runs inside the agent's [sandbox](https://docs.os.meetluno.ai/learn/agents/build-agents.md), not on your machine.

```ts title="src/agents/maintainer.ts" theme={null}
import { defineAcpAgent, defineAgent } from "@meetluno/luno/agent";
import { defineCredential } from "@meetluno/luno/credentials";
import { z } from "zod";

const anthropic = defineCredential({ key: "anthropic", fields: { apiKey: z.string() } });

const claudeCode = defineAcpAgent({
  name: "claude-code",
  description: "A coding agent that reads and edits the repository in /workspace/agent/repo.",
  command: ["npx", "-y", "@zed-industries/claude-code-acp"],
  cwd: "/workspace/agent/repo",
  credentials: [anthropic],
  env: ({ anthropic }) => ({ ANTHROPIC_API_KEY: anthropic.apiKey }),
  permissions: "allow-all",
});

export default defineAgent({
  slug: "maintainer",
  name: "Maintainer",
  description: "Fixes issues in the repository",
  systemPrompt: "Delegate code changes to claude-code, then summarize what changed.",
  model: "anthropic/claude-sonnet-4.6",
  tools: [claudeCode],
});
```

The tool takes `{ prompt }`. The first call starts the process and runs ACP `initialize` and `session/new`; later calls in the same prompt reuse that session. Each call sends `session/prompt` and returns the coding agent's reply plus the tool calls it made. A turn that runs past `timeoutMs` (10 minutes by default) or is aborted kills the process; the next call starts a fresh one. The process stops when the sandbox is disposed at the end of the prompt, so the coding agent's session does not carry over to the next message.

* `permissions` answers the coding agent's permission requests: `read-only` (default) allows reads, searches, and fetches and rejects edits and commands; `allow-all` allows everything. Human approval of individual requests is not supported yet.
* `cwd` must be under `/workspace/agent` (default) or `/workspace/session`.
* `credentials` + `env` resolve secrets into the process environment (in both sandbox modes). Other worker env stays out; in-process mode passes only `PATH`, `HOME`, `USER`, `LANG`, `TMPDIR`, and `SHELL`.
* The sandbox must hold a long-lived process with open stdio. The default in-process sandbox and the local microsandbox VM (`defineSandbox({ mode: "vm" })` without `SPRITES_TOKEN` / `DAYTONA_API_KEY`) can; Sprites and Daytona VMs cannot yet, and the tool fails with a clear error there. In-process mode runs the command on the worker host with the host filesystem (`/workspace/agent` maps to the agent's host directory), so prefer the microsandbox VM for anything you would not run on that machine.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.