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

# Publish an MCP server

> Create a private ModuleX MCP server in the app: choose which workflows, Files, and Knowledge tools it publishes, generate and rotate server keys, and understand the per-organization limits and billing. Step-by-step for organization owners and admins.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

You publish an MCP server from the app, not from code. A **server** is a named, private endpoint for your organization that exposes a chosen set of tools; a **key** authenticates a client that connects to it. This page covers creating a server, choosing its tools, and managing its keys.

<Info>
  **Prerequisites.** Publishing requires an organization on a [paid plan](/billing/plans), and only an [owner or admin](/security/roles-permissions) can create or manage servers and keys. To publish a workflow, it must first be [deployed](/workflow-builder/execution/deploy).
</Info>

## Open ModuleX MCP

Go to **Settings → ModuleX MCP**. The list shows your organization's MCP servers, each with its status, the tools it publishes, and its endpoint URL. This is where you create a new server and open an existing one to manage its tools and keys.

<MediaEmbed id="MX-MEDIA-4710" type="screenshot" caption={"The ModuleX MCP settings tab: the organization's server list with status badges, a copyable endpoint URL per row, and the New MCP server button."} />

## Create a server

Select **New MCP server** and complete the two-step wizard.

<Steps>
  <Step title="Basics">
    Name the server so you can recognize it later (for example, `Support workflows` or `Docs assistant`). The server is created as a **private** endpoint scoped to the current organization.
  </Step>

  <Step title="Setup — choose the tools">
    Pick what the server publishes:

    * **Built-in tools** — toggle the Files and Knowledge tools you want (see [The published tools](/api-reference/mcp/connect-a-client#the-published-tools)). Enabled tools are available to every active key on the server.
    * **Workflow tools** — publish specific deployed workflows, or publish your deployed workflows as a set so newly deployed ones are included automatically. Each published workflow becomes one tool, using its published name, description, and input schema.

    Start from the recommended defaults, or switch to the advanced options to set the initial key's name, expiry, and rate limit while you create the server.
  </Step>

  <Step title="Create and save the first key">
    On create, ModuleX generates the server's first key and shows it **once**. Copy it before you leave the screen — see [Server keys](#server-keys).
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-4720" type="screenshot" caption={"The create-server wizard on the Setup step: built-in Files and Knowledge tool toggles and the workflow selection, with recommended defaults and advanced options."} />

<Note>
  Only workflows that ModuleX has deployed are eligible to be published, and a workflow tool runs that deployed workflow with its published inputs. Publishing does not let a client execute arbitrary workflows by ID — it can call only the tools the server exposes.
</Note>

## Server keys

A **server key** is a static, server-scoped secret with the prefix `mx_mcp_`. A client sends it on every request to authenticate. Keys are independent of your `mx_live_` [API keys](/api-reference/authentication) — an MCP key works only against its own server and carries no other API access.

<Warning>
  A key is shown **once**, at creation or rotation. Copy it immediately — ModuleX does not show it again. If you lose a key, rotate it to issue a new one and invalidate the old.
</Warning>

<MediaEmbed id="MX-MEDIA-4730" type="screenshot" caption={"The one-time key screen: the full mx_mcp_ key with a copy button, its expiry and rate limit, the connection snippet, and the I saved this key acknowledgement that unlocks Done."} />

From a server's **Keys** tab you can:

* **Create** additional keys — for example, one per client or per teammate, so you can revoke one without disrupting the others.
* **Rotate** a key — issue a replacement and retire the current one. Update the client with the new key.
* **Set an expiry** — keys expire after a chosen period (up to a year; a shorter default applies if you do not set one). An expired key stops working and the client must be updated with a current key.

<Tip>
  Give each client its own key. If a key is exposed or a client is decommissioned, rotate or remove just that key and leave the rest working.
</Tip>

## Manage a published server

Open a server to see four tabs:

| Tab          | What it is for                                                                                                |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| **Overview** | The server's status and its endpoint URL, ready to copy into a client.                                        |
| **Tools**    | The published tools — the built-in Files and Knowledge tools, and the workflow tools — and which are enabled. |
| **Keys**     | Create, rotate, and remove keys, and see each key's status and expiry.                                        |
| **Connect**  | Ready-to-paste client configuration. See [Connect an MCP client](/api-reference/mcp/connect-a-client).        |

Changing a server's published tools takes effect for new tool listings; connected clients pick up the change when they next list tools.

## Limits

Servers and keys are bounded per organization and per server:

| Limit                            | Value                                               |
| -------------------------------- | --------------------------------------------------- |
| MCP servers per organization     | 20                                                  |
| Keys per server                  | 10                                                  |
| Keys per organization            | 50                                                  |
| Published tools per server       | 100                                                 |
| Concurrent tool calls per server | 10                                                  |
| Key lifetime                     | Up to 365 days (a shorter default applies if unset) |

If you reach a limit, remove an unused server or key, or narrow a server's published tools, before adding more.

## Billing

Creating a server, listing tools, and connecting a client do not consume credits. A tool call that **runs a workflow** or **searches Knowledge** consumes [credits](/billing/credits) the same as running it in the app, and **File writes** count against your organization's existing Files quotas. When a call cannot be billed — the organization is out of credits, or its plan does not permit the usage — the tool returns an error result instead of running. See [Usage gating & limits](/billing/usage-gating).

## Next steps

<CardGroup cols={2}>
  <Card title="Connect an MCP client" icon="plug" href="/api-reference/mcp/connect-a-client">
    Endpoint, transport, auth, and ready-to-paste configs for Claude Code, Cursor, Codex, and generic HTTP clients.
  </Card>

  <Card title="Deploy a workflow" icon="rocket" href="/workflow-builder/execution/deploy">
    Deploy a workflow so you can publish it as an MCP tool.
  </Card>

  <Card title="ModuleX MCP overview" icon="circle-info" href="/api-reference/mcp/overview">
    What ModuleX MCP is and how it differs from custom MCP servers.
  </Card>

  <Card title="Roles & permissions" icon="shield" href="/security/roles-permissions">
    Who can create and manage MCP servers and keys.
  </Card>
</CardGroup>
