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
1 change: 1 addition & 0 deletions daprdocs/content/en/concepts/terminology.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This page details all of the common terms you may come across in the Dapr docs.
| Dapr control plane | A collection of services that are part of a Dapr installation on a hosting platform such as a Kubernetes cluster. This allows Dapr-enabled applications to run on the platform and handles Dapr capabilities such as actor placement, Dapr sidecar injection, or certificate issuance/rollover. | [Self-hosted overview]({{% ref self-hosted-overview %}})<br />[Kubernetes overview]({{% ref kubernetes-overview %}})
| Dapr Workflows | A Dapr building block for authoring code-first workflows with durable execution that survive crashes, support long-running processes, and enable human-in-the-loop interactions. | [Workflow overview]({{% ref workflow-overview %}})
| HTTPEndpoint | HTTPEndpoint is a Dapr resource use to identify non-Dapr endpoints to invoke via the service invocation API. | [Service invocation API]({{% ref service_invocation_api %}})
| MCPServer | A Dapr resource that declares a connection to an MCP (Model Context Protocol) server for durable tool execution via built-in workflow orchestrations. | [MCPServer resource]({{% ref mcp-server-resource.md %}})
| Namespacing | Namespacing in Dapr provides isolation, and thus provides multi-tenancy. | Learn more about namespacing [components]({{% ref component-scopes %}}), [service invocation]({{% ref service-invocation-namespaces %}}), [pub/sub]({{% ref pubsub-namespaces %}}), and [actors]({{% ref namespaced-actors %}})
| Self-hosted | Windows/macOS/Linux machine(s) where you can run your applications with Dapr. Dapr provides the capability to run on machines in "self-hosted" mode. | [Self-hosted mode]({{% ref self-hosted-overview %}})
| Service | A running application or binary. This can refer to your application or to a Dapr application.
Expand Down
58 changes: 54 additions & 4 deletions daprdocs/content/en/developing-ai/mcp/_index.md
Comment thread
sicoyle marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,60 @@ type: docs
title: "MCP"
linkTitle: "MCP"
weight: 25
description: "Dapr helps developers run secure and reliable Model Context Protocol (MCP) servers"
description: "Dapr helps developers run secure, reliable, and durable Model Context Protocol (MCP) server integrations"
---

### What does Dapr do for MCP servers?
Dapr supports MCP by using its [service invocation API]({{% ref service-invocation-overview.md %}}). Off-the-shelf [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) clients and agent frameworks (LangGraph, the official MCP SDK, custom HTTP clients) point at the local Dapr sidecar and reach MCP servers by App ID. Dapr governs the traffic with the same controls it applies to any other service-to-service call: App ID identity, access policies, HTTP middleware, mTLS, observability, and resiliency.

Using Dapr, developers can interact securely with MCP servers and enable fine-grained ACLs with built-in tracing and metrics, as well as resiliency policies to handle situations where an MCP server might be down or unresponsive.

## How it works

Both the agent and the MCP server run as Dapr apps, each with its own App ID. The MCP client directs requests to its local sidecar and sets the `dapr-app-id` header (or uses the full service-invocation URL). Dapr resolves the target by App ID, applies the policies attached to the MCP server's App ID, and forwards the request.

For each call, Dapr can:

- **Route the request** from the calling app to the target app by App ID.
- **Authenticate the caller's workload identity** using [mTLS]({{% ref mtls.md %}}) with SPIFFE-issued credentials. On by default.
- **Apply access control policies** defined for the target MCP server's App ID — coarse-grained App-ID gating, plus per-tool authorization via [OPA]({{% ref mcp-access-control.md %}}).
- **Apply HTTP middleware** on the inbound pipeline, such as [OAuth 2.0 bearer validation]({{% ref middleware-bearer.md %}}).
- **Capture observability** — logs, metrics, and traces for the call, sliced by caller and target App ID.

Off-the-shelf MCP clients work unchanged — there is no Dapr-specific MCP SDK to adopt for this path.

## Get started

- **[MCP through Dapr service invocation]({{% ref mcp-service-invocation.md %}})** — quickstart and architecture
- **[Authenticating an MCP server]({{% ref mcp-authentication.md %}})** — OAuth 2.0 and bearer middleware
- **[MCP access control]({{% ref mcp-access-control.md %}})** — `Configuration` `accessControl` and OPA for MCP
- **[MCP security posture]({{% ref mcp-security.md %}})** — threat model and defense-in-depth narrative

## Security at a glance

| Layer | What it controls | Reference |
|---|---|---|
| **mTLS + SPIFFE identity** | Every Dapr-to-Dapr call is mutually authenticated using identities Sentry issues and rotates automatically. On by default. | [Dapr mTLS]({{% ref mtls.md %}}) |
| **`Configuration` `accessControl`** | Which caller App IDs may reach which MCP servers. Default-deny is supported. | [MCP access control]({{% ref mcp-access-control.md %}}) |
| **HTTP middleware (bearer / OAuth2)** | Inbound JWT validation on `appHttpPipeline`; outbound token acquisition on `httpPipeline`. | [Authenticating an MCP server]({{% ref mcp-authentication.md %}}) |
| **OPA per-tool policies** | Argument- and tool-aware authorization that inspects the MCP JSON-RPC body. | [MCP access control]({{% ref mcp-access-control.md %}}) |

For the threat-model framing, default postures, and what stays your responsibility, see [MCP security posture]({{% ref mcp-security.md %}}).

## Alternative: the `MCPServer` resource (workflow-centric path)

There is a second way to use MCP with Dapr — the [`MCPServer` resource]({{% ref mcp-server-resource.md %}}). This path turns MCP integration into a deploy-time concern: you declare each MCP server as a YAML resource, and Dapr discovers tools, manages connections, and registers a built-in durable workflow per tool. Calling a tool becomes "start a workflow."

In exchange, you face the following tradeoffs:

- **Requires the [Dapr Workflow]({{% ref workflow-overview %}}) client.** You must invoke MCP tools through the Dapr Workflow SDK, not through your existing MCP client.
- **Off-the-shelf MCP clients and agent frameworks do not work with this path.** If you use LangGraph, the standard MCP Python SDK, or any other framework that speaks the MCP protocol natively, you cannot use these guardrails — you would need to call tools through the workflow SDK and forgo your framework's MCP integration.
- **Scale considerations.** Every tool call spawns a child workflow and writes to the workflow state store. If your agent is already a workflow (for example, a `DurableAgent`), every tool call multiplies into a child workflow.
- **Workflow-client-only today.** Driving `MCPServer`-backed tool calls requires the Dapr Workflow client; off-the-shelf MCP clients cannot drive these flows in the current release.

Use the `MCPServer` resource when you specifically need:

- **Argument-level RBAC, audit, or redaction hooks** on a per-tool basis (`beforeCallTool` / `afterCallTool` / `beforeListTools` / `afterListTools`).
- **Durable retries** that survive a sidecar restart mid-call (backed by Dapr Workflows + Scheduler reminders).
- **Per-tool observability slicing** — one workflow name per tool, so traces, metrics, and audit logs are sliced per-tool out of the box.
- Your application already uses Dapr Workflows for the rest of its execution model.
- You accept that off-the-shelf MCP clients and agent frameworks will not work for these calls.

See the [`MCPServer` resource page]({{% ref mcp-server-resource.md %}}) for the full comparison with the service invocation path and a step-by-step guide.
208 changes: 208 additions & 0 deletions daprdocs/content/en/developing-ai/mcp/howto-use-mcpserver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
type: docs
title: "How-To: Use MCPServer resources"
linkTitle: "How-To: Use MCPServer"
weight: 30
description: "Use MCPServer resources to discover and call tools on MCP servers"
---

This guide walks you through declaring an MCPServer resource, listing its tools, and calling a tool through the Dapr Workflow API. Dapr handles the MCP protocol, transport, authentication, and durable retries — your application just starts workflows by name.

## Step 1: Define the MCPServer resource

Create a file `mcpserver.yaml` in your resources directory:

```yaml
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
name: my-mcp-server
spec:
endpoint:
streamableHTTP:
url: http://localhost:8080
```

This tells Dapr to connect to an MCP server at `http://localhost:8080` using the streamable HTTP transport.

## Step 2: List available tools

Start a `ListTools` workflow using the Dapr Workflow API:

```bash
curl -X POST "http://localhost:3500/v1.0-beta1/workflows/dapr/dapr.internal.mcp.my-mcp-server.ListTools/start" \
-H "Content-Type: application/json" \
-d '{}'
```

Response:
```json
{"instanceID": "abc123"}
```

Poll for the result:

```bash
curl "http://localhost:3500/v1.0-beta1/workflows/dapr/abc123"
```

When `runtimeStatus` is `"COMPLETED"`, the `properties["dapr.workflow.output"]` field contains the tool list. Each tool's `inputSchema` is the raw JSON Schema for its arguments:

```json
{
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city",
"inputSchema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
]
}
```

## Step 3: Call a tool

Each MCP tool gets its own workflow named `dapr.internal.mcp.<server>.CallTool.<tool>`. The tool name is in the workflow name, so the input only carries the arguments:

```bash
curl -X POST "http://localhost:3500/v1.0-beta1/workflows/dapr/dapr.internal.mcp.my-mcp-server.CallTool.get_weather/start" \
-H "Content-Type: application/json" \
-d '{
"arguments": {"city": "Seattle"}
}'
```

Poll for the result as in Step 2. The output is an [MCP `CallToolResult`](https://modelcontextprotocol.io/specification/2025-11-25/schema#calltoolresult) — byte-for-byte the same shape as the MCP wire spec. Each entry in `content` is a flat tagged union with a `type` discriminator:

```json
{
"isError": false,
"content": [
{"type": "text", "text": "Weather in Seattle: sunny, 72°F"}
]
}
```

If the tool call fails at the MCP level (e.g. unknown tool, auth error), `isError` is `true` and the error is in `content`. The workflow itself completes successfully — `isError` is not a workflow failure.

If your call is missing a required argument, you get the same `isError: true` shape immediately — Dapr validates against the tool's cached JSON Schema before contacting the MCP server, so agents/LLMs see actionable errors without burning a network round-trip.

## Step 4 (optional): Add authentication

Add OAuth2 client credentials to authenticate with the MCP server:

```yaml
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
name: my-mcp-server
spec:
endpoint:
streamableHTTP:
url: https://mcp.example.com
auth:
secretStore: kubernetes
oauth2:
issuer: https://auth.example.com/token
clientID: my-client-id
audience: mcp://my-server
secretKeyRef:
name: mcp-oauth-secret
key: clientSecret
```

Dapr fetches a token from the issuer and injects it as a Bearer token on every MCP request. HTTP clients are cached per MCPServer for efficiency.

## Step 5 (optional): Add middleware

Middleware hooks let you run authorization, redaction, and audit as Dapr workflows on every tool call — no agent code change. Hooks are wired in the MCPServer spec and registered as plain workflows in your application (or in a dedicated policy app via `appID`).

### Step 5.1: Add an RBAC hook (deny on policy violation)

```yaml
spec:
middleware:
beforeCallTool:
- workflow:
workflowName: rbac-check
```

Register a workflow named `rbac-check` in your application. It receives an `MCPBeforeCallToolHookInput`:

```text
{ name, toolName, arguments }
```

`name` is the MCPServer resource name; `arguments` is the JSON object the caller passed. Return an error to deny; return nil to allow.

```text
workflow rbac-check(input):
# Argument-level RBAC: inspect the payload and decide.
if input.toolName == "issue_refund":
if input.arguments["amount"] > 10_000:
return error("rbac: refunds over $10K require manual approval")

if input.toolName in DESTRUCTIVE_TOOLS:
if not input.arguments.get("dry_run", false):
return error("rbac: %s requires dry_run=true",
input.toolName)

return ok # nil error so tool call proceeds
```

The hook runs as a durable workflow — if daprd restarts mid-policy-check, Scheduler re-delivers and the decision completes.

> **Caller-keyed RBAC ("which apps can call which tools") belongs at the [`WorkflowAccessPolicy`]({{% ref workflow_api %}}) layer, not the hook.** The hook input doesn't carry caller appID; the policy is. Use the policy as the perimeter and hooks for argument-level decisions.

### Step 5.2: Add a mutating PII redaction hook

To transform `arguments` before they reach the tool — redact PII, normalize values, inject defaults — set `mutate: true`:

```yaml
spec:
middleware:
beforeCallTool:
- workflow:
workflowName: redact-pii
mutate: true
```
Comment thread
sicoyle marked this conversation as resolved.

```text
workflow redact-pii(input):
# input: { name, toolName, arguments }
args = copy(input.arguments)
if "email" in args:
args["email"] = mask_email(args["email"])
return { name: input.name, toolName: input.toolName, arguments: args }
```

The hook returns the same shape it receives. The MCP server (and any subsequent hooks in the chain) sees only the transformed `arguments`.

For after-the-fact response filtering or audit logging, wire the same way under `afterCallTool` — see the [overview examples]({{% ref "mcp-server-resource.md#examples-common-patterns" %}}) for the full set of patterns.

### Step 5.3: Centralize policy on a shared app

To run the hook on a dedicated policy app instead of locally, add `appID`:

```yaml
spec:
middleware:
beforeCallTool:
- workflow:
workflowName: rbac-check
appID: policy-service # runs on the Dapr app named "policy-service"
```
Comment thread
sicoyle marked this conversation as resolved.

The same workflow runs on the named app via service invocation. One shared policy app (RBAC, audit, PII redaction) governs many agent apps without each app embedding the policy. Update the central workflow once; every MCPServer that references it picks up the change without redeploying its callers.

> See the [overview examples]({{% ref "mcp-server-resource.md#examples-common-patterns" %}}) for canonical hook patterns (RBAC, rate limiting, audit, response filtering, tool list filtering).

## Related links

- [MCPServer resource overview]({{% ref mcp-server-resource.md %}})
- [MCPServer spec reference]({{% ref mcpserver-schema %}})
- [Workflow API reference]({{% ref workflow_api %}})
Loading
Loading