Skip to main content

curu-comfy-mcp

What is this

curu-comfy-mcp is an MCP server that lets an AI agent use ComfyUI. The agent sends a workflow, watches the job and gets the output files through MCP tools. Other tools show the models and the node types of the backend, so that the agent can write a workflow that the backend can run. The agent writes no HTTP or WebSocket code. The MCP client starts curu-comfy-mcp on its own computer, or connects to it over HTTP. curu-comfy-mcp is for a person who runs ComfyUI and wants agents, for example Claude Code, to use it. A Python program, for example curu, can also use its ComfyUI client as a library, with no MCP.

What you can do

Task Tool or command Section
Connect an MCP client to one ComfyUI claude mcp add comfy -- curu-comfy-mcp Connect an MCP client
Let a remote client use curu-comfy-mcp curu-comfy-mcp --transport http Serve the tools over HTTP
Let clients sign in with OIDC the [oidc] table Sign in to curu-comfy-mcp with OIDC
Use a ComfyUI behind comfyui_curu_auth CURU_COMFY_MCP_BACKEND_TOKEN, or the oidc table of a backend Reach a backend behind comfyui_curu_auth
Reach a backend that accepts OIDC tokens the oidc table of a backend Reach a backend with its own OIDC client
Use more than one ComfyUI list_backends, the backend input Use more than one backend
Find the models and the node types of the backend list_models, list_node_types, get_node_type Find the models and the node types of the backend
See the queue and the recent jobs get_queue, list_recent_jobs See the queue and the recent jobs
Read the VRAM and the versions of a backend get_system_stats Read and release the VRAM of a backend
Release the VRAM of a backend from Python await client.free(unload_models=True, free_memory=True) Read and release the VRAM of a backend
Upload an input image for a LoadImage node upload_image Upload an input image
Send a workflow submit_workflow Send a workflow
Read or wait for the state of a job get_job, wait_for_job Watch a job
Stop a queued or a running job cancel_job Cancel a job
Get an output file get_output_file Get the output files
Find the cause of a failure the code of the error Handle errors
Run a job from Python, with no MCP from curu_comfy_mcp import ComfyUIClient Use curu-comfy-mcp as a library
Change the backends, the file size limits or the inbound token the configuration file, CURU_COMFY_MCP_* variables, --config, --backend Configure curu-comfy-mcp

Install

You need Python 3.12 or later and uv. Install the curu-comfy-mcp command from PyPI:

uv tool install curu-comfy-mcp

To use the client in your own Python project, add curu-comfy-mcp to the project:

uv add curu-comfy-mcp

The PyPI package comfy-mcp is a different project of Comfy Org. Do not install it.

To install the newest code that has no release yet, use git+https://github.com/darth-veitcher/curu-comfy-mcp in place of curu-comfy-mcp.

You also need a ComfyUI that curu-comfy-mcp can reach over HTTP. ComfyUI needs no custom node.

Quickstart

  1. Start ComfyUI. By default it listens on http://127.0.0.1:8188.

  2. Add curu-comfy-mcp to Claude Code:

    claude mcp add comfy -e CURU_COMFY_MCP_BACKEND_URL=http://127.0.0.1:8188 -- curu-comfy-mcp
    
  3. Start Claude Code, and give it this request:

    Use the comfy tools. Send this workflow, wait for it, and show me the image:
    {"1": {"class_type": "EmptyImage", "inputs": {"width": 64, "height": 64, "batch_size": 1, "color": 16711680}},
     "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0], "filename_prefix": "quickstart"}}}
    

The agent calls submit_workflow, then wait_for_job, then get_output_file. You see a red square of 64 by 64 pixels. This workflow uses only the core nodes of ComfyUI, so it needs no model.

Connect an MCP client

By default, curu-comfy-mcp is a stdio MCP server: the MCP client starts the curu-comfy-mcp command and talks to it on stdin and stdout. Give the URL of the backend in the environment of the command.

For Claude Code:

claude mcp add comfy -e CURU_COMFY_MCP_BACKEND_URL=http://<host>:8188 -- curu-comfy-mcp

Replace <host> with the host name of your ComfyUI.

For a client with a JSON configuration, for example Claude Desktop:

{
  "mcpServers": {
    "comfy": {
      "command": "curu-comfy-mcp",
      "env": {"CURU_COMFY_MCP_BACKEND_URL": "http://<host>:8188"}
    }
  }
}

The client then lists the job tools submit_workflow, get_job, wait_for_job and get_output_file, and the read tools list_backends, list_models, list_node_types, get_node_type, get_queue and list_recent_jobs.

Serve the tools over HTTP

Serve curu-comfy-mcp over HTTP when the agent runs on a different computer, or when a program such as curu connects to it. curu-comfy-mcp then uses the MCP streamable HTTP transport, at the path /mcp.

  1. Make an inbound token. The token must have 32 characters or more.

    export CURU_COMFY_MCP_HTTP_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
    
  2. Start curu-comfy-mcp on HTTP:

    curu-comfy-mcp --transport http --host 0.0.0.0 --port 8000
    
  3. Connect the client with the token. For Claude Code:

    claude mcp add --transport http comfy http://<host>:8000/mcp --header "Authorization: Bearer $CURU_COMFY_MCP_HTTP_TOKEN"
    

The client lists the same tools as on stdio, with the same results and progress notifications.

The inbound token protects curu-comfy-mcp itself. Each request must send it as Authorization: Bearer <token>. A request without the correct token gets HTTP 401 with a WWW-Authenticate: Bearer header, and no tool runs. curu-comfy-mcp does not start on HTTP without a token or an OIDC issuer, so the endpoint cannot be open by mistake.

Option Default Meaning
--transport stdio stdio when the MCP client starts curu-comfy-mcp. http for remote clients.
--host 127.0.0.1 The address that the HTTP server listens on. 127.0.0.1 accepts only clients on the same computer. 0.0.0.0 accepts clients on each network interface.
--port 8000 The port of the HTTP server, from 1 to 65535.
--config none Read this configuration file. The file must exist (see Configure curu-comfy-mcp).
--backend none Use this backend for a tool call that names no backend, in this process only.

curu-comfy-mcp has no TLS, so the token and the results go over the network as plain text. Put a reverse proxy with TLS in front of curu-comfy-mcp on a network that you do not trust.

Sign in to curu-comfy-mcp with OIDC

Let each HTTP client sign in with an access token from your identity provider, for example Authentik or rauthy. curu-comfy-mcp checks each token itself, as an OAuth 2.1 resource server. The static inbound token of Serve the tools over HTTP can stay on. A request then passes with either one.

  1. Register curu-comfy-mcp at your identity provider. Note the issuer, the aud value and the typ of its tokens. docs/oidc-providers.md gives them for each provider.

  2. Write the [oidc] table in the configuration file:

    [oidc]
    issuer = "https://auth.example.com/application/o/comfy-mcp/"
    resource = "https://mcp.example.com/mcp"
    audience = "comfy-mcp"
    token_types = ["at+jwt", "JWT"]
    required_scopes = ["profile"]
    

    The values comfy-mcp in the issuer and in audience are an example of an Authentik client (see Use Authentik). Use the values of your own client.

  3. Start curu-comfy-mcp on HTTP. With an issuer, it needs no static token:

    curu-comfy-mcp --transport http --host 0.0.0.0 --port 8000
    
  4. Let the MCP client get a token from the identity provider. The client sends it as Authorization: Bearer <access token>.

Key Meaning
issuer The issuer URL of the identity provider, exactly as its discovery document gives it. Keep a / at the end if the document has one. It must be https://, or http:// on 127.0.0.1, localhost or ::1.
resource The URL at which clients reach /mcp. Behind a reverse proxy, give the public URL of the proxy. curu-comfy-mcp publishes it in its metadata.
audience The value that the aud claim of each token must hold. The default is resource. Authentik puts the client id of its provider in aud.
token_types The values of typ in the token header that curu-comfy-mcp accepts. The default is ["at+jwt"], the type of RFC 9068. Authentik writes JWT.
required_scopes The scopes that each token must have. The default is no scope. A token without one of them gets HTTP 403.

curu-memory is a different MCP server of the same owner. issuer, resource, audience and token_types have the names and meanings of its CURU_MEMORY_OIDC_* settings. One identity provider can then serve both servers in the same way.

curu-comfy-mcp reads the discovery document of the issuer at the first token, and then the keys at its jwks_uri. It keeps the keys in memory. When a token names an unknown key, curu-comfy-mcp reads the keys again, at most once in 60 seconds. Because of this, a key rotation at the identity provider does not stop curu-comfy-mcp.

curu-comfy-mcp gives HTTP 401 for a token with a wrong issuer, a wrong audience, a bad signature or a past expiry. It also refuses these tokens:

  • A token with an algorithm other than RS256 and ES256, for example HS256.
  • A token with a typ that is not in token_types.
  • A token of the type JWT, or with no type, that has no client id, or that has no scope. An ID token has the same aud as an access token, but it has no scope, so curu-comfy-mcp refuses it.
  • A token that lives more than 1 hour, from iat to exp.

A valid token without a required scope gets HTTP 403 with insufficient_scope. A refused request reaches no tool. Each 401 and 403 has a WWW-Authenticate header with resource_metadata, the URL of the metadata of curu-comfy-mcp. curu-comfy-mcp allows 60 seconds of clock difference for nbf and iat, but none for exp.

An MCP client reads that metadata to find the identity provider. It is at /.well-known/oauth-protected-resource/mcp, with no token:

{"resource": "https://mcp.example.com/mcp", "authorization_servers": ["https://auth.example.com/application/o/comfy-mcp/"], "scopes_supported": ["profile"], "bearer_methods_supported": ["header"]}

The static token carries each required scope, because it is the token of the operator. On stdio, curu-comfy-mcp checks no token. For each job, the log of curu-comfy-mcp names the subject of the token. With no subject, it names the client id.

Use Authentik

A blueprint is a YAML file from which Authentik makes its objects. If you manage Authentik with ansible-ubuntu-server, its authentik role makes this blueprint from roles/authentik/templates/comfy-mcp-oidc.yaml.j2. Set comfy_mcp_oidc_enabled: true to turn it on. Use that role, not a copy of this example.

The example shows what the blueprint makes. It has the same shape as the Authentik client of curu-memory. The name comfy-mcp in it is the slug, the client id and the audience of an example client. It is not the name of the command, and you can use a different name:

version: 1
metadata:
  name: comfy-mcp OIDC
entries:
  - id: group-comfy-mcp
    model: authentik_core.group
    identifiers:
      name: comfy-mcp-users
    attrs:
      is_superuser: false
  - id: provider-comfy-mcp
    model: authentik_providers_oauth2.oauth2provider
    identifiers:
      name: comfy-mcp
    attrs:
      client_type: public
      client_id: comfy-mcp
      redirect_uris:
        - matching_mode: regex
          url: "^http://localhost:[0-9]+/callback$"
        - matching_mode: regex
          url: "^http://127\\.0\\.0\\.1:[0-9]+/callback$"
      authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
      invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
      signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
      access_token_validity: minutes=10
      property_mappings:
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
  - id: app-comfy-mcp
    model: authentik_core.application
    identifiers:
      slug: comfy-mcp
    attrs:
      name: comfy-mcp
      provider: !KeyOf provider-comfy-mcp
  - model: authentik_policies.policybinding
    identifiers:
      target: !KeyOf app-comfy-mcp
      group: !KeyOf group-comfy-mcp
      order: 0
    attrs:
      enabled: true
  • The group comfy-mcp-users and the policy binding give tokens only to the members of the group. This applies to persons and to agents.
  • The provider is a public client, so an MCP client on a laptop signs in with a browser and PKCE, with no client secret. The two redirect URIs accept the callback http://localhost:<port>/callback or http://127.0.0.1:<port>/callback, for example of Claude Code.
  • signing_key makes Authentik sign with RS256 and publish its keys. Without it, Authentik signs with HS256 and the client secret, which curu-comfy-mcp refuses.
  • access_token_validity makes each token live 10 minutes. curu-comfy-mcp refuses a token that lives more than 1 hour.
  • The issuer is https://<authentik host>/application/o/comfy-mcp/, from the slug of the application.
  • aud and azp are comfy-mcp, the client_id of the provider. Because of this, the [oidc] table above sets audience = "comfy-mcp".
  • Authentik writes typ: JWT. Because of this, the table sets token_types = ["at+jwt", "JWT"].

An agent is an Authentik service account with an app password. A service account is an Authentik user for a program, and an app password is a secret of that user. Add the service account to comfy-mcp-users. The agent gets a token with the client credentials grant: it sends client_id, username and password (the app password).

Authentik has no dynamic client registration, so each MCP client uses the client id comfy-mcp.

Use rauthy for development

docs/oidc-providers.md gives the commands that start rauthy with a test client on your computer. The tests of curu-comfy-mcp use the same setup.

Reach a backend behind comfyui_curu_auth

Give curu-comfy-mcp the backend token when ComfyUI runs comfyui_curu_auth. Without the backend token, each tool gives the error backend_auth_failed.

comfyui_curu_auth 2.1.0 and later can also accept an OIDC access token. When the owner of the ComfyUI turns this on, give the backend an oidc table in place of the backend token. See Reach comfyui_curu_auth with an OIDC client.

  1. Copy the credential that comfyui_curu_auth prints on the ComfyUI console when ComfyUI starts. This credential is the backend token.

  2. Set it in the environment of curu-comfy-mcp. For Claude Code:

    claude mcp add comfy -e CURU_COMFY_MCP_BACKEND_URL=http://<host>:8188 -e CURU_COMFY_MCP_BACKEND_TOKEN='<backend-token>' -- curu-comfy-mcp
    

curu-comfy-mcp then sends Authorization: Bearer <backend-token> on each call to the backend and on the /ws connection. curu-comfy-mcp removes the white space at the two ends of the value, so a new-line character from the copy causes no error.

The backend token and the inbound token of Serve the tools over HTTP are two different secrets. The backend token lets curu-comfy-mcp into ComfyUI. The inbound token lets MCP clients into curu-comfy-mcp.

comfyui_curu_auth blocks curu-comfy-mcp after each wrong backend token. The block is 1 second after the first failure, and two times longer after each next failure, to a maximum of 300 seconds. During a block, each tool gives backend_rate_limited with the seconds to wait. To prevent a longer block, curu-comfy-mcp waits 30 seconds after comfyui_curu_auth refuses the backend token on /ws (HTTP 401 or 403). During a block, it waits the time that comfyui_curu_auth gives.

Use more than one backend

Write each ComfyUI in the configuration file, with a name. An agent then gives the name in the backend input of a tool. A tool call with no name uses the default backend.

  1. Write the file ~/.config/curu/curu-comfy-mcp/config.toml. For the other locations, see Configure curu-comfy-mcp:

    default_backend = "local"
    
    [backends.local]
    url = "http://127.0.0.1:8188"
    
    [backends.orange]
    url = "https://orange.example:8443"
    token_env = "ORANGE_COMFY_TOKEN"
    
  2. Give the token of each backend behind comfyui_curu_auth in the variable that its token_env names. For Claude Code:

    claude mcp add comfy -e ORANGE_COMFY_TOKEN='<backend-token>' -- curu-comfy-mcp
    
  • A backend name has 1 to 64 letters, digits, - or _.
  • With two or more backends, name the default backend in default_backend, CURU_COMFY_MCP_DEFAULT_BACKEND or --backend. Else curu-comfy-mcp stops at start.
  • If the variable that token_env names is not set, curu-comfy-mcp stops at start and names the variable.
  • Put no secret in the file. curu-comfy-mcp stops at start when the file has a token key, or a URL with a password.
  • curu-comfy-mcp sends each token only to its own backend.

list_backends gives each backend, the default backend and whether each one answers. It asks each backend at the same time, and waits 5 seconds or less for each:

{"backends": [{"name": "local", "url": "http://127.0.0.1:8188", "default": true, "online": true}, {"name": "orange", "url": "https://orange.example:8443", "default": false, "online": false, "reason": "backend_auth_failed"}]}

reason is the error code of a backend that is not online. The URL never shows a user name or a password.

Each tool that uses a backend has the input backend. For example, send a workflow to orange:

{"workflow": {"9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "cat"}}}, "backend": "orange"}

Each job result, the queue and the list of recent jobs give backend, the name of their backend:

{"job_id": "8f1c2e7a-0d5b-4e43-9a51-2c7e6f0b9d14", "queue_number": 3, "backend": "orange"}

get_job, wait_for_job and get_output_file find the backend of a job when you give no name. curu-comfy-mcp asks the backend that it sent the job to. After a restart of curu-comfy-mcp, it asks the default backend first, then the other backends at the same time. curu-comfy-mcp stores no file for this, because the backends hold each job.

Reach a backend with its own OIDC client

Give a backend an OIDC client when the backend accepts an access token from an identity provider. curu-comfy-mcp then gets its own access token for that backend, with the OAuth 2.0 client credentials grant. It never sends the token of an MCP client to a backend, because the MCP specification forbids that.

  1. Register curu-comfy-mcp at the identity provider of the backend, as a confidential client with the client credentials grant. Note its client id and its client secret.

  2. Write the oidc table of the backend in the configuration file:

    [backends.orange]
    url = "https://orange.example:8443"
    
    [backends.orange.oidc]
    issuer = "https://auth.example.com/auth/v1/"
    client_id = "comfy-mcp-orange"
    client_secret_env = "ORANGE_OIDC_SECRET"
    resource = "https://orange.example:8443"
    
  3. Set the client secret in the variable that client_secret_env names. For Claude Code:

    claude mcp add comfy -e ORANGE_OIDC_SECRET='<client-secret>' -- curu-comfy-mcp
    
Key Meaning
issuer The issuer of the identity provider. curu-comfy-mcp reads the token endpoint from its discovery document. It must be https://, or http:// on a loopback host.
token_endpoint The token endpoint. Give it in place of issuer when the provider has no discovery document.
client_id The client id of curu-comfy-mcp at the provider.
client_secret_env The variable that holds the client secret. The secret never goes into the file.
username The user name of an Authentik service account. With it, the secret is the app password of that account.
scope The scope of the token request, with spaces between the scopes.
audience The audience of the token request. Auth0 needs it.
resource The resource of the token request (RFC 8707). rauthy puts it in aud when the client allows it.
auth_method client_secret_post (the default) sends the secret in the form. client_secret_basic sends it in an HTTP Basic header.

curu-comfy-mcp asks for a token at the first call to the backend, and keeps it in memory. It gets a new token 60 seconds before the old one expires, or at half the lifetime of a token of less than 2 minutes. Calls at the same time share one token request. curu-comfy-mcp sends the token as Authorization: Bearer <token> on each HTTP call and on the /ws connection.

When the backend refuses a token with HTTP 401, curu-comfy-mcp gets a new token and sends the call one more time. When curu-comfy-mcp cannot get a token, each tool for that backend gives backend_token_failed, and list_backends gives it as the reason.

  • A backend has token_env or an oidc table, not both. Else curu-comfy-mcp stops at start.
  • curu-comfy-mcp also stops at start when a backend with an oidc table has a user name in its URL. It also stops when that backend is the default and CURU_COMFY_MCP_BACKEND_TOKEN is set.
  • curu-comfy-mcp stops at start when the file has a client_secret key, or when the variable of client_secret_env is not set.
  • The single-backend variables give no OIDC client. Write the backend in the file.
  • The backend sees curu-comfy-mcp as the caller, not the user. With inbound OIDC, the log of curu-comfy-mcp names the user of each job.

docs/oidc-providers.md gives the values for each identity provider, and what a backend must check.

Reach comfyui_curu_auth with an OIDC client

comfyui_curu_auth (the gate) 2.1.0 and later accept an OIDC access token, and also the backend token. The owner of the ComfyUI turns this on with two settings of the gate: COMFYUI_CURU_AUTH_OIDC_ISSUER_URL and COMFYUI_CURU_AUTH_OIDC_AUDIENCE. An older comfyui_curu_auth accepts only the backend token, so use token_env for it.

  1. Ask the owner of the ComfyUI for the issuer and the audience of the gate.
  2. Set issuer of the oidc table to the issuer of the gate.
  3. Make the token hold the audience of the gate in aud. With rauthy, set resource to the audience, and add it to allowed_resources of the client. With Authentik, the audience is the client id of the provider of the gate.
  4. If the owner of the ComfyUI set COMFYUI_CURU_AUTH_OIDC_REQUIRED_SCOPES, give the client each of those scopes.

The gate refuses a token with a lifetime of more than 1 hour, an HS256 token and an ID token. When it refuses a token on an HTTP call, it gives HTTP 401 also during a block. curu-comfy-mcp then gets a new token and sends the call one more time. If the gate refuses the new token too, the tool gives backend_auth_failed, not backend_rate_limited. A valid token passes also during a block. On /ws, curu-comfy-mcp gets a new token and waits 30 seconds before it connects again.

When the gate has no keys of the identity provider, it gives HTTP 503. Then each tool gives backend_unavailable, and the message names HTTP 503. A 503 does not make the block longer.

The provider guide of the gate gives the settings for each provider.

Find the models and the node types of the backend

Read the backend before you write a workflow. Each backend has its own models and node packs, and a workflow with a wrong name fails. These tools only read, and they send no workflow.

  1. list_models with no folder gives the model folders, for example checkpoints, loras, vae and text_encoders.

  2. list_models with a folder gives the file names of that folder:

    {"folder": "checkpoints", "contains": "krea"}
    
  3. list_node_types finds the node types. Give contains for the name or the display name, or category for the menu path, for example loaders.

  4. get_node_type gives the inputs and the outputs of one node type:

    {"node_type": "CLIPLoader"}
    

Each node type gives its name (the class_type of a workflow node), its display_name, its category and output_node. output_node is true for a node that ends a workflow.

Each input gives required, its type, its default, and its min and max when it has them. required is false for an optional input. A list input has the type COMBO, and options gives its valid values, for example each file that a loader accepts. Each output gives its index, the slot of a link such as ["8", 0].

Some custom nodes declare a list input as plain text, STRING. Then the backend gives no valid values.

The lists of list_models and list_node_types stop at limit, from 1 to 500, with the default 200. total gives the count after the filter. truncated is true when the list stops at the limit: then give a narrower filter.

The valid values of one input stop at 500, and the valid values of all the inputs stop at 1000. options_total gives the count of valid values after options_contains, and options_truncated is true when they stop. To find one model file in a long list, give options_contains:

{"node_type": "LoraLoader", "options_contains": "krea"}

See the queue and the recent jobs

get_queue gives the job that runs now and the waiting jobs, in order. The first waiting job runs next. Other users can have jobs in the same queue. running and pending give the job_id and the queue_number of each job. The waiting jobs stop at limit, from 1 to 500, with the default 200. pending_total gives the count of waiting jobs, and pending_truncated is true when they stop. backend gives the name of the backend of the queue.

list_recent_jobs gives the last finished jobs, newest first. jobs gives the job_id, the state (succeeded or failed) and the output_count of each job. limit is from 1 to 500, with the default 20. backend gives the name of the backend of the jobs.

Neither tool has a text filter, because a job id is a random value. The history of ComfyUI gives no count, so list_recent_jobs has truncated but no total.

Read and release the VRAM of a backend

get_system_stats reads the devices of the backend and the versions of its software. VRAM is the memory of the GPU. Use it to see whether a large model fits before you send a workflow.

{"backend": "default", "system": {"os": "linux", "comfyui_version": "0.27.0", "python_version": "3.12.3", "pytorch_version": "2.7.1+cu128"}, "devices": [{"name": "cuda:0 NVIDIA GeForce RTX 3090 : cudaMallocAsync", "type": "cuda", "index": 0, "vram_total": 25296044032, "vram_free": 19978125312, "torch_vram_total": 6442450944, "torch_vram_free": 12582912}]}
  • backend is the name of the backend that gave the result.
  • devices gives each device of the backend. ComfyUI sends one device: the device that it runs on. On a computer with no GPU, its type is cpu and it has no index.
  • vram_total and vram_free are the memory of the device and its free part, in bytes. 25296044032 bytes is about 23.6 GiB.
  • torch_vram_total is the memory that PyTorch reserves for ComfyUI, in bytes. ComfyUI can release at least this memory. It is only a lower bound. Under DynamicVRAM, ComfyUI keeps the model weights outside this memory, and the value reads about 0.
  • torch_vram_free is the free part of the memory that PyTorch reserves, in bytes.
  • system gives the operating system, the RAM in bytes and the versions of ComfyUI, Python and PyTorch.
  • A field is absent when the backend does not send it. An older ComfyUI sends no torch_vram_total and no template versions. A field with a value of the wrong type is absent too.

The result does not give the command line of ComfyUI, because it can hold a secret.

From Python, await client.system_stats() gives the same result as a SystemStats. await client.free(unload_models=True, free_memory=True) releases the VRAM of the backend. unload_models unloads the models from the GPU. free_memory also clears the cache of the node outputs, so the next job runs each node again. The two arguments have no default. Give them by name.

free has an effect on each user of the backend. Their results do not change, but their next job loads its models again. For this reason, no MCP tool releases VRAM. ComfyUI releases the memory on the next loop of its prompt worker, so the VRAM can go down a short time after free returns. Read the system stats again to see the change.

Upload an input image

Upload an image to the backend before a workflow that reads it, for example an image-to-image workflow. Give the bytes of the image to upload_image in base64:

{"image": "iVBORw0KGgo=", "name": "cat.png"}

The result gives the stored name of the file:

{"name": "cat.png", "folder_type": "input", "backend": "default"}

Put the stored name into the image input of a LoadImage node, then send the workflow:

{"workflow": {"1": {"class_type": "LoadImage", "inputs": {"image": "cat.png"}}, "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0], "filename_prefix": "cat"}}}}
  • image is the base64 text of the bytes of the file, with no data: prefix. The text can have line breaks. The tool does not read a file path, because curu-comfy-mcp can run on a different computer.
  • name is the file name on the backend. With no name, the name is the SHA-256 hash of the bytes and an extension, for example .png. Thus the same bytes always get the same name.
  • folder_type is input, temp or output. The default is input, the folder that LoadImage reads.
  • subfolder puts the file into a subfolder of that folder. The stored name then starts with the subfolder, for example refs/cat.png.
  • overwrite is false by default. The backend then keeps a file of the same name. For the same bytes, the name stays the same. For other bytes, the backend stores a new file, for example cat (1).png.
  • Set overwrite to true to replace a file. Other users of the backend can read the same files, so replace only your own files.
  • backend names the backend. Upload to the backend that runs the workflow.
  • The result name is the name that the backend used. Always put that name into the workflow, not the name that you sent.

CURU_COMFY_MCP_MAX_UPLOAD_BYTES sets the upload size limit, 25 MiB by default. The limit counts the decoded bytes, not the base64 text. A larger image gives the error upload_too_large. A wrong input gives the error invalid_upload, for example a name with /, \ or .., or text that is not base64. The error table gives each case. For each refused input, curu-comfy-mcp sends nothing to the backend.

From Python, upload_image of the client gives the stored name:

from pathlib import Path

from curu_comfy_mcp import ComfyUIClient


async def upload(url: str, path: str) -> str:
    data = Path(path).read_bytes()
    async with ComfyUIClient(url) as client:
        return await client.upload_image(data)

Send a workflow

submit_workflow sends a workflow in the ComfyUI API format to the backend. In the ComfyUI web UI, use "Export (API)" to get this format. Each key is a node id. Each value has class_type and inputs.

{"workflow": {"9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "cat"}}}}

The result gives the job id and the queue number:

{"job_id": "8f1c2e7a-0d5b-4e43-9a51-2c7e6f0b9d14", "queue_number": 3, "backend": "default"}
  • job_id is the ComfyUI prompt id. Give it to the other tools.
  • queue_number is the number that ComfyUI gives to the job. A higher number is a later job.
  • backend is the name of the backend that got the workflow. With no configuration file, the one backend is default.

curu-comfy-mcp sends the workflow unchanged. It does not check the nodes or the models. ComfyUI checks the workflow, and curu-comfy-mcp gives each refusal back to the agent.

Watch a job

get_job reads the state of a job now. wait_for_job waits until the job is finished, or until its timeout comes.

{"job_id": "8f1c2e7a-0d5b-4e43-9a51-2c7e6f0b9d14", "timeout_seconds": 300}

The state of a job is one of these values:

State Meaning Other fields
queued The job waits in the queue. queue_position: 1 is the next job to run.
running The backend runs the job now. node_id, node_type, progress_value, progress_max
succeeded The job finished with no error. outputs: each output file. The list can be empty.
failed A node failed, or the ComfyUI /interrupt call stopped the job. failure
unknown The backend has no job with this id. A job that cancel_job removed from the queue is also unknown. none

For a running job, progress_value is the current step of the node, and progress_max is its last step. For example, a sampler with 20 steps goes from 1 to 20. Some nodes send no progress, so these two fields are not always there. Each node starts at step 1 again.

timeout_seconds is from 1 to 3600, and the default is 300. After a timeout, the result gives the state queued or running, and the job continues. Call wait_for_job again to wait more.

When the MCP client asks for progress, wait_for_job sends a progress notification for each change. Example: "Job 8f1c… is running node 3 (KSampler): step 12/20."

Cancel a job

cancel_job stops one job. It removes a queued job from the queue, or it interrupts a running job. Give the job_id. You can also give the backend, as for get_job.

{"job_id": "8f1c2e7a-0d5b-4e43-9a51-2c7e6f0b9d14"}

The result gives the outcome:

{"job_id": "8f1c2e7a-0d5b-4e43-9a51-2c7e6f0b9d14", "backend": "default", "outcome": "interrupted"}
Outcome Meaning What to do next
interrupted The job ran, and curu-comfy-mcp asked the backend to stop it. Call wait_for_job. The state is failed with the cause interrupted. It is succeeded when the job finished before the stop.
dequeued The job waited in the queue, and curu-comfy-mcp removed it. The job does not run. Nothing. get_job now gives the state unknown.
not_found The job is not in the queue now. It finished before or during the cancel, or the backend does not know it. curu-comfy-mcp changed nothing. Nothing. This is not an error.

On ComfyUI v0.27.0 or later, a cancel never stops a job with a different id. curu-comfy-mcp first reads the queue of the backend. It interrupts the job only when the job runs, and it sends the job id with the interrupt. ComfyUI v0.27.0 then stops the running job only when it has that id. curu-comfy-mcp never sends an interrupt with no job id, and never clears the queue.

Each caller who can see a job id can cancel that job, and that includes a job of another user. get_queue and list_recent_jobs show the id of each job. The tool has the MCP annotation destructive_hint, so the MCP client can ask you before it calls the tool.

A second cancel of the same job gives not_found. If the backend is down, the result is the error backend_unavailable, not not_found.

Get the output files

The outputs of a succeeded job give each file:

{"node_id": "9", "filename": "cat_00001_.png", "subfolder": "", "folder_type": "output", "media_type": "image/png"}
  • folder_type is output for a saved file, or temp for a preview.
  • subfolder is the sub-folder in that folder. It can be empty.
  • media_type comes from the file extension. A media_type that starts with image/ gives image content.

Give the job_id, the node_id and the filename to get_output_file. An image comes back as MCP image content, and the client can show it. Another file, for example a video, comes back as an MCP embedded resource with its bytes in base64.

get_output_file gives only a file from the outputs of that job. It cannot read other files from the backend.

Use curu-comfy-mcp as a library

Use the ComfyUI client from your own Python program. The client needs no MCP, no configuration file and no CURU_COMFY_MCP_ variable. It runs in asyncio. The /ws listener uses the asyncio client of websockets, so the client does not run in trio.

import anyio

from curu_comfy_mcp import ComfyClientError, ComfyUIClient, Frame

WORKFLOW = {
    "1": {
        "class_type": "EmptyImage",
        "inputs": {"width": 64, "height": 64, "batch_size": 1, "color": 0},
    },
    "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0], "filename_prefix": "lib"}},
}


async def run(url: str, token: str | None, workflow: dict) -> None:
    async def on_frame(frame: Frame) -> None:
        print("frame", frame.type)

    async with ComfyUIClient(url, token=token) as client:
        try:
            job_id, _ = await client.submit(workflow)
            state = await client.wait(job_id, on_frame=on_frame)
            for output in state.outputs or []:
                content = await client.output_file(
                    job_id, output.node_id, output.filename, 10_000_000
                )
                print(output.filename, content.content_type, len(content.data))
        except ComfyClientError as error:
            print(error.code, error.message)


anyio.run(run, "http://127.0.0.1:8188", None, WORKFLOW)
  • ComfyUIClient(url, token=...) makes a client for one backend. For a backend behind comfyui_curu_auth, give its token. For a token that changes, give token_source= in its place (see below).
  • async with opens the /ws connection for the life of the client. Make one client for each backend, and keep it open.
  • submit sends the workflow unchanged, and gives the job id and the queue number.
  • wait returns the state of the job when it is finished. With no time limit, it waits for as long as the job runs. Give a number of seconds as the second argument to stop the wait earlier. After that time, the job continues, and the state is queued or running.
  • on_frame gets each /ws frame of the job, in order. frame.type is the type that ComfyUI sends, for example progress_state, progress, executing or the type of a custom node. frame.data is its data. Use the frames to see that a job still runs, for example to reset a stall window. A frame that arrives before wait starts does not reach on_frame.
  • on_change gets each new state of the job, as wait_for_job reports it.
  • cancel(job_id) stops the job, as cancel_job does, and gives a CancelResult. Its outcome is interrupted, dequeued or not_found (see Cancel a job). Its backend is None.
  • upload_image(data, name=None, *, folder_type="input", subfolder="", overwrite=False, max_bytes=None) stores an image, and gives the stored name for a LoadImage node (see Upload an input image). max_bytes is your own size limit, or None for no limit. A wrong input raises InvalidUpload, and a larger image raises UploadTooLarge. Neither sends a request.
  • system_stats gives the devices and the versions of the backend, and free releases its VRAM (see Read and release the VRAM of a backend).
  • output_file gives an OutputContent. Its fourth argument is the size limit in bytes. A larger file raises OutputTooLarge. content.data holds the bytes. content.content_type is the Content-Type header that the backend sent, or None when it sent no header. content.file.media_type comes from the file extension.
  • Each failure raises a subclass of ComfyClientError. Its code is a code of Handle errors, for example backend_unavailable.

A token source is an object with two methods. async def token(self) -> str gives the current token. def invalidate(self, token: str) -> None drops a token that the backend refused. The client asks for the token before each HTTP call and each /ws connection. After HTTP 401 on an HTTP call, it calls invalidate and tries one more time with a new token. After HTTP 401 on the /ws handshake, it calls invalidate and connects again after 30 seconds.

To use the named backends of a configuration file, read the settings with Settings.load(). It reads the file and the environment, as the command does, and raises ConfigError for a bad value. Then use async with Backends(settings) as backends:, and get a client with await backends.client("orange"). Give None in place of a name to get the client of the default backend. A backend with an oidc table gets its token from a ClientCredentials token source.

Import each name from curu_comfy_mcp. The names in curu_comfy_mcp.__all__ are the public names. Each other name and each module path can change in a minor version.

Handle errors

Each failure of a tool is an error result. Its text is one JSON object with code, message and, for some codes, details. The agent reads code to decide what to do.

Code Cause What to do
invalid_workflow The workflow is not a JSON object. Send an object in the API format.
workflow_refused ComfyUI refused the workflow. details.node_errors names each node and input. Fix each node, then send the workflow again.
backend_unavailable curu-comfy-mcp cannot reach the backend, or the backend gives an HTTP error. details.backend_url gives the URL, and message gives the reason. Start ComfyUI, or correct the URL.
backend_auth_failed The backend refused the backend token of curu-comfy-mcp (HTTP 401 or 403). details.status gives the status. Ask the operator to correct the backend token and to start curu-comfy-mcp again. The token is in the variable of token_env, or in CURU_COMFY_MCP_BACKEND_TOKEN (see Reach a backend behind comfyui_curu_auth). For a backend with an OIDC client, ask the operator to check its oidc table: audience, resource and scope.
backend_token_failed curu-comfy-mcp could not get an access token for the backend from its identity provider. details.backend names the backend. details.token_endpoint gives the token endpoint, or the issuer when curu-comfy-mcp could not read the discovery document. details.error gives the OAuth error code, for example invalid_client, or null. Ask the operator to check client_id, the client secret and the identity provider (see Reach a backend with its own OIDC client).
backend_rate_limited comfyui_curu_auth blocks curu-comfy-mcp for a time after a wrong backend token (HTTP 429). details.retry_after_seconds gives the wait: from 1 to 301 seconds with comfyui_curu_auth, longer after more wrong tokens. It is null when the backend sends no Retry-After. Wait that time, then try again.
unknown_backend The configuration has no backend with this name. details.known_backends lists the names. Use a name from that list, or call list_backends.
unknown_job wait_for_job or get_output_file got a job id that the backend does not know. get_job gives the state unknown in its place. Use a job id from submit_workflow.
unknown_model_folder The backend has no model folder with this name. details.known_folders lists the folders. Use a folder from that list.
unknown_node_type The backend has no node type with this name. Find the name with list_node_types.
unknown_output The job has no output with this node and file name. Use a node_id and a filename from outputs.
job_not_finished The job is queued, running or failed, so it has no outputs. For a queued or running job, call wait_for_job first. For a failed job, read failure.
output_too_large The file is larger than the size limit. Ask the operator to set a larger CURU_COMFY_MCP_MAX_OUTPUT_BYTES and to start curu-comfy-mcp again (see Configure curu-comfy-mcp).
invalid_upload curu-comfy-mcp refused an input of upload_image, and sent nothing to the backend. details.field names the input: image, name or subfolder. details.value gives a refused name or subfolder. For image, send the bytes in base64, with no data: prefix. For name, give a file name with no /, \ or .., or no name. For subfolder, give parts with / between them, and no \ or ...
upload_too_large The decoded image is larger than the upload size limit. curu-comfy-mcp sent nothing to the backend. details.size_bytes and details.max_upload_bytes give the two sizes. Send a smaller image, or ask the operator to set a larger CURU_COMFY_MCP_MAX_UPLOAD_BYTES and to start curu-comfy-mcp again (see Configure curu-comfy-mcp).

A refusal also gives data that helps the agent. For a list input with a wrong value, the message says "Value not in list" and gives each valid value. ComfyUI checks only the output nodes, such as SaveImage, and the nodes upstream of them. A node with no path to an output node gives no error and does not run.

A refused workflow gives an error like this:

{"code": "workflow_refused", "message": "ComfyUI refused the workflow. Fix each node in details.node_errors.", "details": {"error": "Prompt outputs failed validation", "node_errors": [{"node_id": "4", "node_type": "CheckpointLoaderSimple", "input_name": "ckpt_name", "message": "Value not in list: ckpt_name: 'x.safetensors' not in [...]"}]}}

A node that fails while it runs does not give an error result. The job gets the state failed, and failure gives the cause:

Field Meaning
code node_failed for an exception in a node. interrupted when the ComfyUI /interrupt call stopped the job.
node_id, node_type The node that failed. These fields are absent when ComfyUI does not give them.
exception_type The Python exception, for example torch.OutOfMemoryError when the GPU has too little memory. This field is absent when ComfyUI does not give it.
message For node_failed, the exception message, at most 500 characters. For interrupted, a fixed text.

Configure curu-comfy-mcp

curu-comfy-mcp reads its settings when it starts, from four layers. A higher layer replaces a lower layer value by value:

  1. The command-line options --config and --backend.
  2. The environment variables below.
  3. The configuration file. curu-comfy-mcp reads --config, else CURU_COMFY_MCP_CONFIG, else $XDG_CONFIG_HOME/curu/curu-comfy-mcp/config.toml. With no XDG_CONFIG_HOME, it reads ~/.config/curu/curu-comfy-mcp/config.toml.
  4. The defaults.

A file that --config or CURU_COMFY_MCP_CONFIG names must exist. A missing file at the default location is not an error. The file holds default_backend, max_output_bytes, max_upload_bytes and the [backends.<name>] tables, with url and token_env (see Use more than one backend). It can also hold the [oidc] table (see Sign in to curu-comfy-mcp with OIDC). Each backend can have an oidc table (see Reach a backend with its own OIDC client). An unknown key stops curu-comfy-mcp, so that you find a typing error at start.

An empty variable has the same effect as an unset variable.

Variable Default Meaning
CURU_COMFY_MCP_CONFIG none The path of the configuration file. The file must exist.
CURU_COMFY_MCP_DEFAULT_BACKEND none The name of the backend for a tool call with no backend name.
CURU_COMFY_MCP_BACKEND_URL http://127.0.0.1:8188 The URL of the default backend. It must be an http:// or https:// URL with a host. With no file, it describes the one backend, default.
CURU_COMFY_MCP_MAX_OUTPUT_BYTES 26214400 (25 MiB) The largest output file that get_output_file sends. It must be a whole number of bytes, more than 0.
CURU_COMFY_MCP_MAX_UPLOAD_BYTES 26214400 (25 MiB) The largest image that upload_image sends, after curu-comfy-mcp decodes the base64. It must be a whole number of bytes, more than 0.
CURU_COMFY_MCP_HTTP_TOKEN none The inbound token for HTTP. It must have 32 characters or more. On stdio, curu-comfy-mcp does not read it.
CURU_COMFY_MCP_OIDC_ISSUER none The issuer of the OIDC access tokens. With no issuer, OIDC is off. It replaces issuer of [oidc].
CURU_COMFY_MCP_OIDC_RESOURCE none The URL at which clients reach /mcp. OIDC needs it. It replaces resource of [oidc].
CURU_COMFY_MCP_OIDC_AUDIENCE the resource URL The value that aud of each token must hold. It replaces audience of [oidc].
CURU_COMFY_MCP_OIDC_TOKEN_TYPES at+jwt The values of typ that curu-comfy-mcp accepts, with a comma between two values, for example at+jwt,JWT. It replaces token_types of [oidc].
CURU_COMFY_MCP_OIDC_REQUIRED_SCOPES none The scopes that each token must have, with a space between two scopes. It replaces required_scopes of [oidc].
CURU_COMFY_MCP_BACKEND_TOKEN none The bearer token of the default backend, behind comfyui_curu_auth. With no value, curu-comfy-mcp sends no Authorization header to it.

The MCP client sends each file through the conversation of the agent, so a large file uses much of its context. Keep the limit low unless the agent needs large files, for example videos.

CURU_COMFY_MCP_BACKEND_URL can hold a user name and a password, for a reverse proxy with Basic authentication. A URL in the file cannot hold a password, because no secret goes into the file. curu-comfy-mcp sends the user name and the password in the Authorization header. Thus a backend cannot have both a user name in its URL and a token.

If a value is not correct, curu-comfy-mcp writes one line to stderr and stops with exit code 78. This is EX_CONFIG of sysexits.h, the usual code for a configuration error. HTTP without a valid token or an issuer also gives exit code 78. An unknown option or a port outside 1 to 65535 gives exit code 2, the usual code for a usage error. If the port is in use, curu-comfy-mcp writes the address to stderr and stops with exit code 3.

Limits

  • curu-comfy-mcp does not select a backend by load or by cost. The agent names the backend, or the default applies.
  • curu-comfy-mcp gets a backend token only with the client credentials grant. It does not use token exchange, so a backend sees curu-comfy-mcp, not the user.
  • curu-comfy-mcp cannot use Google as the identity provider of a backend, because Google has no client credentials grant.
  • curu-comfy-mcp gives no access by user or by group. Each valid token with the required scopes can use each tool. It has no user accounts.
  • curu-comfy-mcp reads only JWT access tokens signed with RS256 or ES256. It cannot read an opaque token, so it does not support Google as the issuer.
  • curu-comfy-mcp gives no tokens and serves no login page. The identity provider does that.
  • curu-comfy-mcp has no TLS. Use a reverse proxy for TLS.
  • curu-comfy-mcp does not check a workflow before it sends it. Read the backend with the read tools, and let ComfyUI check the workflow.
  • A list of the read tools stops at 500 items. A larger list needs a filter.
  • curu-comfy-mcp does not start, stop or install a backend.
  • curu-comfy-mcp records no owner of a job. Each caller who can see a job id can cancel that job.
  • On a ComfyUI older than v0.27.0, the backend ignores the job id of an interrupt. If the job finishes just before the interrupt, the interrupt can stop the next job of the queue. That job can be the job of a different user.
  • curu-comfy-mcp cannot delete an uploaded file, because ComfyUI has no call for that. It does not upload masks with /upload/mask.
  • No MCP tool releases the VRAM of a backend. Only the library can call free.
  • free does not wait until the VRAM goes down. Read the system stats again to see it.
  • The library does not keep the /ws frames of a job that arrive before wait starts. Call wait at once after submit.
  • curu-comfy-mcp keeps no data. After a restart, it reads each job state from the history of the backends. If ComfyUI restarts, its history is empty, and each old job becomes unknown.
  • After a restart, if the backend of a job is off, a call with no backend name cannot find the job. get_job then gives the state unknown, and wait_for_job and get_output_file give unknown_job. If the default backend is off, the call gives backend_unavailable.

Technical notes

  • The project was comfy-mcp before 1.0.0. The command does not read the old COMFY_MCP_ variables or ~/.config/curu/comfy-mcp/config.toml. Move the file to ~/.config/curu/curu-comfy-mcp/config.toml (ADR-014).
  • curu-comfy-mcp uses the official MCP Python SDK, version 2.3, and the websockets package. One install gives the command and the library (ADR-012). The library imports no MCP, so a program that uses it does not load the SDK.
  • The public library surface is curu_comfy_mcp.__all__. Semantic versioning covers those names, the tool names and arguments, the result fields, the error codes and the settings. The names load at their first use, so the command still starts fast (ADR-008). The contract is in contracts/public-api.md.
  • A test compares the public surface with a committed snapshot that holds the version of the package. A change of the surface needs a new version and a changelog section (ADR-013). The snapshot has no descriptions, because semantic versioning does not cover them. The contract is in contracts/surface-snapshot.md.
  • A wait gets each /ws frame of its job. A frame that does not change the state does not move the next read of the history. Thus wait_for_job reads the backend as often as before.
  • It uses these ComfyUI calls: POST /prompt, GET /queue, POST /queue, POST /interrupt, GET /history/{prompt_id}, GET /history?max_items=N, GET /view, GET /ws, GET /models, GET /models/{folder}, GET /object_info, GET /object_info/{node_type}, GET /system_stats, POST /free and POST /upload/image. The wire formats come from ComfyUI v0.27.0.
  • A cancel reads GET /queue first. For a running job, it sends POST /interrupt with {"prompt_id": ...}. For a queued job, it sends POST /queue with {"delete": [...]}, then reads GET /queue again, because the job can start in that time. If the job runs then, it sends POST /interrupt with its prompt_id, and the outcome is interrupted. If the job is in the history, it ran to its end, and the outcome is not_found. Else the outcome is dequeued. The trust boundary is the backend: each caller who can see a job id can cancel that job (ADR-010).
  • The history and the queue give the state of a job. /ws gives only the running node and its progress, and wakes each wait. When /ws is lost, curu-comfy-mcp connects again, and the history still gives the final state.
  • free is a library call only, with no MCP tool, because /free unloads the models of each user of the backend (ADR-011).
  • The design has three layers: the ComfyUI client, the MCP server and the command. Only the command knows the transport (ADR-001).
  • Each tool returns a failure as an error result with a JSON text, not as a ToolError (ADR-002).
  • The inbound token check uses the token-verifier hook of the MCP SDK, and compares the token in constant time (ADR-003).
  • The OIDC check uses pyjwt. It reads the discovery document and the JWKS with httpx2, and never turns off the TLS check. The SDK serves the metadata of RFC 9728, and curu-comfy-mcp checks aud itself (ADR-007). The contract is in contracts/auth.md.
  • The backend token goes on the HTTP client and on the /ws handshake. After a handshake refused with HTTP 401 or 403, /ws waits 30 seconds. After HTTP 429, it waits the Retry-After time. After a different refusal, for example 502, it keeps the fast schedule. Progress can stop for the time of a wait. The history still gives the final state (ADR-004). The error contract is in contracts/errors.md.
  • On HTTP, the access log shows each request path without its query string. A token that a client puts in the query string by mistake does not go into the log.
  • When --host is 127.0.0.1, localhost or ::1, the SDK also accepts only local Host and Origin headers. This protects against DNS rebinding from a web page.
  • The configuration has four layers in one order: the flags, the environment, the file and the defaults. The file is in the directory of the curu tools (ADR-006). The contract is in contracts/config.md.
  • The token of a backend comes from curu_comfy_mcp.oauth.ClientCredentials. An httpx2 auth flow puts the current token on each HTTP call, and /ws asks for it before each handshake. After a /ws handshake that the backend refuses with HTTP 401, curu-comfy-mcp drops the token. The contracts are in contracts/config.md and contracts/errors.md.
  • curu-comfy-mcp opens a client for a backend at the first tool call that uses it. list_backends reads GET /queue and opens no /ws.
  • The lists of list_models and list_node_types have a filter, a limit, total and truncated. get_queue has a limit, pending_total and pending_truncated. list_recent_jobs has a limit and truncated (ADR-005).
  • The tool contract is in contracts/tools.md. The contract of the read tools is in contracts/tools.md. The command and HTTP contract is in contracts/http.md.
  • The license is MIT. See LICENSE.

Develop

git clone https://github.com/darth-veitcher/curu-comfy-mcp
cd curu-comfy-mcp
uv sync
uv run pytest

uv run pytest runs the default tests against a fake ComfyUI on a loopback port. They need no network, no GPU and no model. To run the live test against a real ComfyUI:

CURU_COMFY_MCP_BACKEND_URL=http://<host>:8188 uv run pytest -m integration

Run the quality gates before a pull request:

uv run ruff check --fix && uv run ruff format
uv run ty check
beacon doctor --strict

A test keeps the public surface the same at each version. The public surface is each item of semantic versioning: the tools, the results, the error codes, the public names and the settings. The snapshot of that surface is in tests/contract/surface.json. When you change an item, the test fails and shows each change. Then do these steps:

  1. Increase __version__ in src/curu_comfy_mcp/__init__.py. Increase MAJOR for a removed or changed item, or for a new required argument. Increase MINOR for a new optional item.
  2. Add a section for the new version to CHANGELOG.md.
  3. Write the snapshot again:
uv run python -m tests.support.surface

The command prints each difference. It writes nothing when the surface changed and the version did not. If no tag has that version yet, add --same-version. Do not edit the snapshot by hand.

AGENTS.md gives the rules for each change.

Glossary

  • Access token: a token that an identity provider gives to a client. The client sends it to a service as a bearer token.
  • Agent: an AI model that calls tools, for example Claude in Claude Code.
  • API format: the JSON form of a ComfyUI workflow that the /prompt call accepts. The web UI makes it with "Export (API)".
  • Backend: one ComfyUI server that curu-comfy-mcp sends jobs to. Each backend has a name in the configuration.
  • Backend token: the bearer token that curu-comfy-mcp sends to the backend. comfyui_curu_auth calls it the credential.
  • Bearer token: a secret that a client sends in the Authorization header to prove that it may connect.
  • Audience: the service that a token is for. It is in the aud claim of the token.
  • base64: a text encoding of binary bytes.
  • Claim: one value in a JWT, for example iss or exp.
  • Client credentials grant: the OAuth way for a program to get an access token with its own id and secret, with no user.
  • Configuration file: the TOML file that names the backends of curu-comfy-mcp. By default it is at $XDG_CONFIG_HOME/curu/curu-comfy-mcp/config.toml or ~/.config/curu/curu-comfy-mcp/config.toml.
  • Default backend: the backend of a tool call that gives no backend name.
  • Device: a GPU or a CPU that ComfyUI runs the models on.
  • Discovery document: the JSON document at <issuer>/.well-known/openid-configuration. It gives the addresses of an identity provider.
  • DNS rebinding: an attack in which a web page in a browser sends requests to a server on the local computer.
  • DynamicVRAM: a mode of ComfyUI that keeps the model weights outside the memory of PyTorch.
  • Embedded resource: an MCP result part that holds a file, its URI and its media type.
  • Frame: one message that ComfyUI sends on its /ws connection. The frame of a job has the job id in its data.
  • Gate: comfyui_curu_auth, a ComfyUI custom node that refuses each call without a valid backend token or access token.
  • Inbound token: the bearer token that each HTTP client sends to curu-comfy-mcp. It is not a credential of a backend.
  • Identity provider: a server that signs in users and programs, and gives them access tokens, for example Authentik or rauthy.
  • Image content: an MCP result part that holds an image and its media type. The client can show it.
  • Interrupt: a request to ComfyUI to stop the job that runs now. ComfyUI stops it at its next check: usually the next sampler step, or the start of the next node.
  • Issuer: the URL that names an identity provider. It is in the iss claim of each token.
  • Job: one run of a workflow on the backend. Its id is the ComfyUI prompt id.
  • JWKS: the JSON Web Key Set, the public keys of an identity provider. curu-comfy-mcp checks the signature of a token with them.
  • JWT: a JSON Web Token, a signed token with claims. curu-comfy-mcp reads JWT access tokens.
  • List input: a node input with a fixed list of valid values.
  • MCP: the Model Context Protocol, the protocol that an agent uses to find and call tools.
  • Model folder: a kind of model file on the backend, for example checkpoints.
  • Node: one step of a workflow, for example a sampler or an image save.
  • Node pack: a set of custom node types that a person installs in ComfyUI.
  • Node type: a kind of node that the backend can run, for example KSampler. Its name is the class_type of a workflow node.
  • Output node: a node that ends a workflow, for example SaveImage.
  • OIDC: OpenID Connect, a sign-in standard on top of OAuth 2.0.
  • Opaque token: an access token that only its identity provider can read. It is not a JWT.
  • Operator: the person who configures and starts curu-comfy-mcp in the MCP client.
  • Progress notification: an MCP message that tells the client how far a tool call is.
  • Public surface: each item that semantic versioning covers. These are the tools and their results, the error codes and the names in curu_comfy_mcp.__all__. They also include the command options, the variables and the configuration keys. The public library surface, curu_comfy_mcp.__all__, is one part of it.
  • Protected-resource metadata: the document of RFC 9728 that tells an MCP client the issuer and the scopes of curu-comfy-mcp.
  • Resource server: a service that accepts access tokens and checks them. curu-comfy-mcp is one on HTTP.
  • Reverse proxy: a server, for example Caddy or nginx, that receives the requests and sends them to curu-comfy-mcp.
  • Scope: a name for a right that a token gives, for example profile.
  • Semantic versioning: a version of three numbers, MAJOR.MINOR.PATCH. A change that can break a caller increases MAJOR. A new feature increases MINOR.
  • SHA-256: a hash function. It gives a value of 64 hex characters for a set of bytes. In practice, different bytes give a different value.
  • Snapshot: a committed file that holds the public surface at one version. A test compares the code with it.
  • Stall window: the time that a caller waits for a sign of life from a job, before it stops the job.
  • stdio: standard input and output. The MCP client and the server talk through them.
  • Stored name: the name of an uploaded file on the backend, with its subfolder. A LoadImage node reads the file with this name.
  • Streamable HTTP: the MCP transport over HTTP. A client sends each request to one endpoint, and the server can stream the answer.
  • TLS: the encryption of HTTP traffic, as in HTTPS.
  • Token source: an object that gives the current token of one backend (token), and drops a token that the backend refused (invalidate).
  • Trust boundary: the set of callers that can change the jobs of the other callers. For curu-comfy-mcp, it is each caller who can see a job id on a backend.
  • Upstream: a node is upstream of a different node when its output goes to that node, directly or through other nodes.
  • VRAM: the memory of a GPU. A model must be in VRAM to run on the GPU.
  • Workflow: a graph of nodes that ComfyUI runs.

Metadata

Release files for curu-comfy-mcp 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for curu-comfy-mcp 1.0.0
File Size Uploaded Provenance Actions
curu_comfy_mcp-1.0.0.tar.gz 252.5 kB With provenance Details for curu_comfy_mcp-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for curu-comfy-mcp 1.0.0
File Interpreter ABI Platform Actions
curu_comfy_mcp-1.0.0-py3-none-any.whl Python 3 none any Details for curu_comfy_mcp-1.0.0-py3-none-any.whl

Total release size: 321.7 kB

Release files / curu_comfy_mcp-1.0.0.tar.gz

Download URL curu_comfy_mcp-1.0.0.tar.gz
Size 252.5 kB
Tags Source
SHA-256 checksum
How to use checksums
e0680edd7fe767851199f7005deeb76b5ee6a66e4c28e324aa5398c5a3a8e646
BLAKE2b-256 checksum
How to use checksums
d9acb4221176261bfda7f6e9c52285c89bb812b1d07e6da830794dec188f2894
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / curu_comfy_mcp-1.0.0-py3-none-any.whl

Download URL curu_comfy_mcp-1.0.0-py3-none-any.whl
Size 69.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
59ec5197d992243f7eedffa1ae885bfbdd720b8828a785680954433b98baee2b
BLAKE2b-256 checksum
How to use checksums
cfdc91f12f5264192e298412cade8272d7d9a2ed435c7a771144d7f447add73d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for curu-comfy-mcp 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for curu_comfy_mcp-1.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page