MCP Protocol Integration for Dify Workflows#
Overview#
Dify's workflow-as-MCP-server feature lets you expose any Dify application — including Workflow, Chat, Advanced Chat, Agent, and Completion apps — as a tool that external Model Context Protocol (MCP) clients can discover and invoke. Once a Dify app is published as an MCP server, AI assistants such as Claude Desktop, custom AI pipelines, or any other MCP-compatible runtime can use it as a first-class tool, passing structured inputs and receiving structured outputs through a standard JSON-RPC interface.
The single entry-point for all MCP traffic is:
POST /mcp/server/<server_code>/mcp
The server_code is a 16-character token that identifies the server and acts as the access credential — no separate API key or bearer token is required . A Dify workspace administrator creates this server token through the console, binding it to a specific app .
What You Can Do#
| Scenario | How Dify MCP Helps |
|---|---|
| Run a Dify Workflow from Claude Desktop | Register the workflow as an MCP server; Claude invokes it as a native tool call |
| Build a multi-agent pipeline | Connect any MCP-aware orchestrator to Dify to leverage its RAG, model routing, and workflow logic |
| Expose a chat agent as a tool | Dify's chat and agent-chat apps are supported alongside workflows |
Protocol Version History#
Dify's MCP server initially advertised protocol version 2024-11-05 to maximise client compatibility. PR #37892 upgraded the server to negotiate up to MCP 2025-06-18 — the latest revision of the spec at the time of writing — enabling the flagship 2025-06-18 feature (structured tool output) while keeping all older clients byte-for-byte compatible .
Architecture#
Server Configuration Model#
Each MCP server is represented by an AppMCPServer database record that links a Dify app to an external-facing server_code . The key fields are:
| Field | Description |
|---|---|
server_code | Globally unique 16-character token; used in the endpoint URL and as the access credential |
app_id | The Dify app that backs this MCP server |
tenant_id | The workspace the server belongs to |
name | Display name, automatically synced to the app name |
description | Human-readable description surfaced as the tool description in tools/list |
parameters | JSON object mapping each workflow input variable name to a description shown to MCP clients |
status | active, inactive, or normal; only active servers accept requests |
Each workspace can have at most one MCP server per app (a unique constraint on tenant_id, app_id), and every server_code is globally unique .
The server_code is generated by AppMCPServer.generate_server_code(16), which draws cryptographically random characters from the ASCII alphanumeric set and checks the database for collisions before returning .
Request Lifecycle#
When a POST arrives at /mcp/server/<server_code>/mcp, the controller:
- Parses the JSON-RPC payload and validates it against
MCPRequestPayload. - Resolves the negotiated protocol version from the
MCP-Protocol-Versionrequest header (added in PR #37892 — see Protocol Version Negotiation). - Looks up the
AppMCPServerand associatedAppbyserver_code, then checks the server status isactive. - Extracts the app's
user_input_formto build the tool'sinputSchema. - Routes notifications (e.g.
notifications/initialized) to a lightweight handler that returns HTTP 202 with an empty body, and routes requests to the full handler pipeline . - For requests, retrieves or lazily creates an
EndUserrecord scoped to the MCP server , then dispatches tohandle_mcp_requestincore/mcp/server/streamable_http.py.
Supported App Modes#
The following Dify app types can be published as MCP servers:
workflow— exposes all workflow inputs and returns theoutputsdict asstructuredContent(for protocol2025-06-18+)chatandadvanced-chat— require aqueryinput; return the answer stringagent-chat— same as chat but uses streaming internally; answer is assembled fromagent_thoughteventscompletion— user-defined input variables map to tool parameters; answer returned as text
File-type inputs (file, file_list, external_data_tool) are silently skipped when building the tool schema .
Protocol Version Negotiation#
Dify's MCP server supports three protocol revisions and negotiates the best shared version with each connecting client. Behavior is gated on the negotiated version so that older clients continue to work without any changes.
Supported Versions#
| Version | Notes |
|---|---|
2024-11-05 | Oldest supported revision; no structured output |
2025-03-26 | Default when the MCP-Protocol-Version header is absent |
2025-06-18 | Latest; enables structured tool output (outputSchema + structuredContent) |
Prior to PR #37892, the server unconditionally advertised 2024-11-05. The upgrade introduced the SERVER_SUPPORTED_PROTOCOL_VERSIONS frozenset and updated SERVER_LATEST_PROTOCOL_VERSION to 2025-06-18 .
initialize Handshake#
Version negotiation takes place during the initialize request. The client sends its desired version in params.protocolVersion; the server responds with that version if it is in the supported set, or falls back to SERVER_LATEST_PROTOCOL_VERSION (2025-06-18) if the requested version is unknown :
// Client → Server
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0" }
}
}
// Server → Client (echoes the requested version back)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "Dify", "version": "..." },
"instructions": "<server description>"
}
}
If the client requests an unrecognized version (e.g. "1999-01-01"), the server returns "2025-06-18" — its own latest — rather than an error, per the MCP lifecycle specification.
MCP-Protocol-Version Header (Post-initialize Requests)#
After the initialize handshake, clients should include the MCP-Protocol-Version HTTP header on every subsequent request to signal which version was negotiated. The server reads this header to decide whether to include 2025-06-18 features :
| Header value | Resolved version |
|---|---|
| Absent or empty | 2025-03-26 (spec back-compat default) |
2024-11-05 | 2024-11-05 |
2025-03-26 | 2025-03-26 |
2025-06-18 | 2025-06-18 |
| Anything else | Error — see below |
An unsupported header value causes the server to return a JSON-RPC INVALID_REQUEST error (code -32600) that echoes the request's id field. This ensures the client can correlate the error with its original request :
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32600,
"message": "Unsupported MCP-Protocol-Version: 1999-01-01"
}
}
Exception — notifications: Fire-and-forget notifications (e.g. notifications/initialized) always return HTTP 202 regardless of the header value, because notifications never receive a response .
Exception — initialize requests: The initialize method negotiates via the request body; its header is never validated or rejected, so older clients that send a stale header during initialization are unaffected .
Version-Specific Features#
2024-11-05 — Baseline#
Clients negotiating 2024-11-05 receive the original tool interface:
tools/listreturns aToolobject withname,description, andinputSchema. NotitleoroutputSchemafields are present.tools/callreturns aCallToolResultwith a singleTextContentitem. For workflow apps the text is the JSON-serializedoutputsdict; for chat/agent modes it is the answer string.
This behavior is preserved unchanged after PR #37892 — the new fields remain None for these clients and are stripped by exclude_none=True serialization .
2025-03-26 — Transitional#
2025-03-26 is the version assumed when a client omits the MCP-Protocol-Version header after the initialize handshake . At the response level it behaves identically to 2024-11-05 — no structured output fields are included — making it a safe default for any client that has not yet adopted header-based version signaling.
2025-06-18 — Structured Tool Output#
The MCP specification introduced structured tool output in revision 2025-06-18 . Dify surfaces this feature for clients that negotiate this version:
tools/list Changes#
The Tool object gains two new fields:
| Field | Value in Dify | Purpose |
|---|---|---|
title | App name | Human-readable display name distinct from the machine-readable name |
outputSchema | {"type": "object"} | Declares that the tool returns a JSON object; clients and LLMs can rely on this for strict parsing |
Example tools/list result for a 2025-06-18 client:
{
"tools": [
{
"name": "my_workflow",
"title": "my_workflow",
"description": "Runs the data-processing pipeline",
"inputSchema": {
"type": "object",
"properties": {
"input_text": { "type": "string", "description": "Text to process" }
},
"required": ["input_text"]
},
"outputSchema": { "type": "object" }
}
]
}
tools/call Changes#
CallToolResult gains a structuredContent field alongside the existing content array . The content of structuredContent depends on the app mode:
Workflow apps — structuredContent is the raw outputs mapping from the workflow execution, giving callers direct access to named output variables without parsing JSON from a text string:
{
"content": [{ "type": "text", "text": "{\"summary\": \"...\", \"score\": 0.9}" }],
"structuredContent": { "summary": "...", "score": 0.9 }
}
Chat, Advanced Chat, Agent, and Completion apps — structuredContent wraps the answer string under the key "answer", providing a consistent JSON-object shape:
{
"content": [{ "type": "text", "text": "Paris is the capital of France." }],
"structuredContent": { "answer": "Paris is the capital of France." }
}
Note: Per the MCP specification, when a server returns
structuredContentit should also return the serialized form in aTextContentblock for backwards compatibility . Dify always does this — both fields are always present for 2025-06-18 clients.
API Reference#
Endpoint#
POST /mcp/server/<server_code>/mcp
Content-Type: application/json
MCP-Protocol-Version: <negotiated_version> # Required on post-initialize requests
The server_code token uniquely identifies the MCP server and doubles as the access credential. No Authorization header is needed .
Supported JSON-RPC Methods#
| Method | Type | Description |
|---|---|---|
initialize | Request | Opens a session and negotiates protocol version |
tools/list | Request | Returns the list of tools (one per MCP server) |
tools/call | Request | Invokes the workflow and returns the result |
ping | Request | Health-check; returns an empty result |
notifications/initialized | Notification | Signals end of initialization; returns HTTP 202 |
initialize#
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0" }
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "Dify", "version": "1.x.x" },
"instructions": "A description of what this workflow does"
}
}
The protocolVersion in the response is the negotiated version the server will use for the rest of the session. If the client requested an unsupported version, the server returns its own latest version .
tools/list#
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
Response (protocol 2024-11-05 / 2025-03-26):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "my_workflow",
"description": "Runs the data-processing pipeline",
"inputSchema": {
"type": "object",
"properties": {
"input_text": { "type": "string", "description": "Text to process" },
"language": { "type": "string", "enum": ["en", "fr", "de"], "description": "Output language" }
},
"required": ["input_text"]
}
}
]
}
}
Response (protocol 2025-06-18): — adds title and outputSchema:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "my_workflow",
"title": "my_workflow",
"description": "Runs the data-processing pipeline",
"inputSchema": { "...": "..." },
"outputSchema": { "type": "object" }
}
]
}
}
tools/call#
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "my_workflow",
"arguments": {
"input_text": "Summarise this article for me.",
"language": "en"
}
}
}
Response (protocol 2024-11-05 / 2025-03-26):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "{\"summary\": \"...\", \"keywords\": [\"a\", \"b\"]}" }
]
}
}
Response (protocol 2025-06-18): — adds structuredContent:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "{\"summary\": \"...\", \"keywords\": [\"a\", \"b\"]}" }
],
"structuredContent": {
"summary": "...",
"keywords": ["a", "b"]
}
}
}
Input Schema Mapping#
The tool's inputSchema is built from the app's configured user_input_form. Each supported variable type maps to a JSON Schema type :
| Dify Variable Type | JSON Schema Type | Notes |
|---|---|---|
text-input, paragraph | "string" | |
select | "string" with "enum" | Options list included |
number | "number" | |
checkbox | "boolean" | |
json-object | "object" | Custom JSON Schema properties forwarded if defined |
file, file_list, external_data_tool | (skipped) | Not representable in MCP tool schemas |
For chat, advanced-chat, agent-chat apps, a required "query" string parameter is always prepended to the input schema — this is the main user message sent to the app .
For completion and workflow apps, no implicit "query" parameter is added; the schema reflects only the declared input variables .
Error Responses#
| Error | Code | Trigger |
|---|---|---|
INVALID_REQUEST | -32600 | Server not found, server not active, unsupported MCP-Protocol-Version header |
INVALID_PARAMS | -32602 | Malformed request payload, invalid input form variable types |
METHOD_NOT_FOUND | -32601 | Unrecognized JSON-RPC method |
INTERNAL_ERROR | -32603 | Unexpected server-side failure |
All errors follow the standard JSON-RPC 2.0 error envelope. The id field always echoes the request's id (or null if the request had no id) .
Setting Up an MCP Server in Dify#
Prerequisites#
- A published Dify application (Workflow, Chat, Advanced Chat, Agent Chat, or Completion).
- Admin or editor access to the Dify workspace.
Step 1 — Create the MCP Server#
Call the console API to register the app as an MCP server :
POST /console/api/apps/<app_id>/server
Authorization: Bearer <console_token>
Content-Type: application/json
{
"description": "Summarises text and extracts keywords",
"parameters": {
"input_text": "The raw text you want to summarise",
"language": "ISO 639-1 language code for the output (e.g. 'en')"
}
}
The description becomes the tool's description shown to LLM clients — make it concise and action-oriented. The parameters object maps each workflow input variable name to a human-readable description; these descriptions are included in the tool's inputSchema and guide the LLM when filling in arguments.
On success, the API returns HTTP 201 with the server_code token:
{
"id": "...",
"name": "My Summariser",
"server_code": "aBcDeFgHiJkLmNoP",
"description": "Summarises text and extracts keywords",
"status": "active",
"parameters": { "input_text": "...", "language": "..." },
"created_at": 1234567890,
"updated_at": 1234567890
}
The server_code is used in all MCP client configurations. Keep it secure — it is the only credential required to invoke your workflow.
Step 2 — Verify Server Status#
Only servers with status: "active" accept MCP requests . Servers are set to active automatically on creation. If needed, activate an existing server:
PUT /console/api/apps/<app_id>/server
Content-Type: application/json
{
"id": "<server_id>",
"description": "...",
"parameters": { "..." },
"status": "active"
}
Step 3 — Rotate the Server Code (Optional)#
If you need to revoke access and issue a new credential:
GET /console/api/apps/<server_id>/server/refresh
Authorization: Bearer <console_token>
This generates a new server_code and invalidates the old one. All MCP clients must be reconfigured with the new token .
Connecting MCP Clients#
Once the MCP server is active and you have its server_code, configure any MCP-compatible client to connect. The Dify MCP endpoint uses the HTTP Streamable transport defined by the MCP specification.
Claude Desktop#
Claude Desktop supports MCP servers configured in ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent path on your OS. Add a new entry under mcpServers:
{
"mcpServers": {
"my-dify-workflow": {
"command": "uvx",
"args": [
"mcp-client-http",
"--url", "https://<your-dify-host>/mcp/server/aBcDeFgHiJkLmNoP/mcp"
]
}
}
}
Tip: Replace
aBcDeFgHiJkLmNoPwith your actualserver_codeand<your-dify-host>with your Dify instance's domain. If you are self-hosting Dify locally, usehttp://localhostor your Docker host address.
After saving the file and restarting Claude Desktop, your Dify workflow will appear as an available tool in the tool picker. Claude will automatically negotiate the protocol version during the initialize handshake and send the appropriate MCP-Protocol-Version header on subsequent requests.
HTTP-Based MCP Clients (General)#
Any MCP client that supports the HTTP Streamable transport can connect to the same endpoint. The general pattern is:
- Initialize — send an
initializerequest with your preferredprotocolVersion(2025-06-18recommended). - Send the initialized notification —
POST notifications/initialized(returns HTTP 202). - List tools — send
tools/list; the server returns the Dify app as a single tool. - Invoke — send
tools/callwith the tool name and arguments.
Include MCP-Protocol-Version: 2025-06-18 in the HTTP headers for steps 2–4 if you want structured output. Omit the header (or use 2024-11-05) for the legacy text-only response format.
Python Example (raw HTTP)#
import requests
BASE_URL = "https://<your-dify-host>/mcp/server/aBcDeFgHiJkLmNoP/mcp"
HEADERS = {"Content-Type": "application/json", "MCP-Protocol-Version": "2025-06-18"}
# 1. Initialize
init_resp = requests.post(BASE_URL, json={
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "my-client", "version": "1.0"}
}
})
print(init_resp.json()["result"]["protocolVersion"]) # "2025-06-18"
# 2. Initialized notification
requests.post(BASE_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
headers=HEADERS)
# 3. Call the tool
call_resp = requests.post(BASE_URL, json={
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "my_workflow", "arguments": {"input_text": "Hello world", "language": "en"}}
}, headers=HEADERS)
result = call_resp.json()["result"]
print(result["structuredContent"]) # {"summary": "...", "keywords": [...]}
print(result["content"][0]["text"]) # same data serialized as JSON string
Workflow Design Best Practices#
Designing Inputs for LLM Callers#
When an LLM calls your Dify tool, it reads the inputSchema to decide what arguments to provide. Investing effort in input design pays dividends in accuracy:
- Write clear, action-oriented parameter descriptions. These are the
parametersvalues you set when creating the MCP server. A description like"The raw English-language text to summarise (max 2000 words)"is far more useful to an LLM than"input". - Mark only truly required fields as required. Optional fields give the LLM flexibility; required fields that the LLM can't always fill lead to failed invocations.
- Prefer scalar types over JSON objects where possible —
string,number,boolean, andselect(enum) are easiest for LLMs to populate correctly. Usejson-objectinputs only when the caller can reliably construct the nested structure. - Avoid file inputs.
fileandfile_listvariable types are silently excluded from the MCP schema and cannot be provided by remote callers .
Designing Outputs for 2025-06-18 Clients#
For workflow apps used with 2025-06-18 clients, the workflow's named output variables are directly surfaced in structuredContent. Consider:
- Use meaningful output variable names (e.g.
summary,sentiment,keywords) because they become keys instructuredContentthat callers read programmatically. - Keep output types JSON-serializable. Objects, arrays, strings, numbers, and booleans all work. Non-serializable Python objects will raise an error.
- Minimise the number of output nodes. A tool with three well-named outputs is easier to consume than one with fifteen.
For chat apps, structuredContent always contains {"answer": <string>}. The design guidance here is the same as for any Dify chat app: keep system prompts focused and return actionable, concise answers.
Security Considerations#
- Treat
server_codelike an API key. It grants unauthenticated access to your workflow. Do not embed it in public client-side code. - Rotate the code immediately if you suspect it has been leaked, using the
/console/api/apps/<server_id>/server/refreshendpoint . - Set servers to
inactivewhen not in use to prevent unintended invocations . - Validate workflow inputs with Dify's built-in variable constraints (max length, required flags, enum options) — these are enforced server-side even for MCP callers.
- Apply rate limits at the infrastructure layer (reverse proxy / API gateway) to protect against abusive MCP clients.
Backward Compatibility and Implementation Notes#
Backward Compatibility Guarantee#
PR #37892 was designed so that no existing MCP client needs to change anything . The guarantee works at two levels:
- Version negotiation — a client that sends
"protocolVersion": "2024-11-05"in itsinitializerequest receives"2024-11-05"back, exactly as before. - Response serialization — all 2025-06-18 fields (
title,outputSchema,structuredContent) default toNonewhen the negotiated version is older than2025-06-18. Pydantic'sexclude_none=Trueonmodel_dumpstrips these fields from the serialized JSON, so the wire format is byte-for-byte identical to the pre-upgrade behavior .
Clients that omit the MCP-Protocol-Version header after initialization are treated as 2025-03-26 per the spec's backward-compatibility rule . This also means they will not receive structured output fields, providing a safe default.
Version Threading#
Once the protocol version is resolved from the MCP-Protocol-Version header (or from the initialize body), it is threaded through the entire call stack:
MCPAppApi.post()
→ negotiate_protocol_version() # resolve from header
→ _process_mcp_message(..., protocol_version)
→ _handle_request(..., protocol_version)
→ _handle_mcp_request(..., protocol_version)
→ handle_mcp_request(..., protocol_version) # core dispatcher
→ handle_list_tools(..., protocol_version)
→ handle_call_tool(..., protocol_version)
This ensures that every handler that gates behavior on the version receives it without needing global state or thread-locals .
Version Comparison Approach#
The _supports_structured_output helper uses lexical string comparison to determine whether a version meets the 2025-06-18 threshold. This works correctly because MCP protocol versions are YYYY-MM-DD strings, so lexical order equals chronological order :
STRUCTURED_OUTPUT_MIN_VERSION = "2025-06-18"
def _supports_structured_output(protocol_version: str) -> bool:
return protocol_version >= STRUCTURED_OUTPUT_MIN_VERSION
No Schema or Database Changes#
The 2025-06-18 upgrade required no database migrations and no changes to the Pydantic MCP type definitions — the Tool and CallToolResult models already carried the 2025-06-18 fields (title, outputSchema, structuredContent) as optional (None) fields. The upgrade only changed the logic that decides when to populate them .
Test Coverage#
The upgrade ships with a comprehensive test suite , with 415 unit tests passing in tests/unit_tests/core/mcp/. Coverage includes:
initializeecho/fallback for every supported version and non-string inputMCP-Protocol-Versionheader resolution for absent, supported, and unsupported values- Structured-output gating at the
2025-03-26boundary - Version threading through the
handle_mcp_requestdispatcher - Legacy serialization pinned at the
model_dumplevel extract_structured_outputacross all app modes