-
Notifications
You must be signed in to change notification settings - Fork 780
MCP Server Resource Type for durable MCP Servers and Tools #5160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,471
−6
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
f968487
docs(mcp): init docs for mcpserver resource
sicoyle 751193b
feat: add more indepth details everywhere
sicoyle daabfcc
feat: add bit more clarity on new resource middleware
sicoyle 3f7ff36
Merge branch 'v1.18' into feat/mcp-crd
sicoyle c75652a
style: address copilot feedback
sicoyle 4ed6ca8
Merge branch 'v1.18' into feat/mcp-crd
sicoyle feda898
Merge branch 'v1.18' into feat/mcp-crd
sicoyle 5101bee
Merge branch 'v1.18' into feat/mcp-crd
msfussell 5420bfd
Merge branch 'v1.18' into feat/mcp-crd
sicoyle 48c5ceb
Merge branch 'v1.18' into feat/mcp-crd
sicoyle 61dadd3
refactor: revamp after conversations/feedback
sicoyle 71a5b66
Merge branch 'feat/mcp-crd' of github.com:sicoyle/docs into feat/mcp-crd
sicoyle 7161b00
Merge branch 'v1.18' into feat/mcp-crd
sicoyle 5cd10f9
fix: address yael feedback
sicoyle 6d0fc4d
Merge branch 'v1.18' into feat/mcp-crd
sicoyle df8fc04
Merge branch 'v1.18' into feat/mcp-crd
sicoyle 3e19bf1
fix: address final yael feedback
sicoyle c700165
Merge branch 'feat/mcp-crd' of github.com:sicoyle/docs into feat/mcp-crd
sicoyle 0604592
fix: address final yael feedback
sicoyle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
daprdocs/content/en/developing-ai/mcp/howto-use-mcpserver.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` | ||
|
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" | ||
| ``` | ||
|
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 %}}) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.