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

# Custom apps and MCP

> Add custom apps, credentials, and MCP servers.

Use a custom app when the built-in catalog does not cover the service you need, or when you're connecting to an internal API. Search the live catalog first:

```bash theme={null}
luno integrations search "<service or capability>"
luno integrations actions list --search "<action>"
```

Before writing code, read the service's current API or MCP documentation. Prefer an authoritative `llms.txt`, OpenAPI or GraphQL schema, or API reference over inferred endpoints and payloads.

## Two layers: catalog + code

A credentialed custom integration has two parts. Do not skip either when the user should connect the app in the product UI.

| Layer | What it does | How |
| - | - | - |
| **Catalog registration** | Makes the app appear in **Integrations** / `luno connect` so someone can enter a secret | `luno integrations create` (manual fields or `--openapi` / `--mcp` / `--graphql`) |
| **Code authoring** | Declares the app credential once and builds actions that use it | `defineIntegration()` in `src/integrations/`, then `app.action(...)` |

Code-only `defineIntegration` does **not** register an org catalog app. Without `integrations create`, the service will not show up in the Connect dialog, and `luno connect <slug>` will not work for it.

Actions that need **no** credentials stay as plain `defineAction` in `src/actions/` — no app required.

## End-to-end happy path

1. **Search the catalog** — confirm the app is missing (`luno integrations search`).
2. **Register the app** — `luno integrations create` so it is connectable in the org.
3. **Connect** — user enters the secret in the web Integrations flow (`luno connect <slug>` or the MCP `connect_app` link), or `luno credentials create` for a static key you already have in the shell.
4. **Author in code** — `defineIntegration` + `app.action(...)` (or `luno integrations sync <slug>` to scaffold the app module from the platform template).
5. **Deploy** — ship actions/agents/workflows that use those tools.

When a public OpenAPI, GraphQL, or MCP URL is available, prefer URL-based create so auth and fields are detected:

```bash theme={null}
luno integrations create --openapi <url> --preview
luno integrations create --mcp <url> --preview
luno integrations create --graphql <url> --preview
```

Remove `--preview` after reviewing the assembled request. Use only the flag matching the source.

For a manual API-key app:

```bash theme={null}
luno integrations create \
  --name "Acme" \
  --description "Internal Acme billing API" \
  --logo https://example.com/acme-logo.png \
  --field apiKey:secret
```

`--description` defaults to `--name` when omitted. At least one `--field` is required. Optional `--logo` is a public image URL shown in the Integrations catalog and Connect UI. In the web dashboard, click the logo placeholder on create to upload an image or paste a URL.

Custom org apps are registered as **`{organization}/{name}`** (for example `wells/demo-echo`), not a bare `demo-echo` slug. Use that full id for `connect`, `credentials create`, `integrations sync`, and `defineIntegration({ slug })`. After create, prefer:

```bash theme={null}
luno integrations sync <org>/<name>
```

That writes `src/integrations/<name>/integration.ts` with the correct slug and credential fields.

`luno integrations get <org>/<name>` returns `{ kind: "custom", app: { … } }` with the credential template. Built-in catalog apps return `{ kind: "catalog", app: { package, … } }` instead.

`integrations actions list` means different things by app kind:

| App kind | What `integrations actions list` returns |
| - | - |
| **Official catalog** (`github`, …) | Ready toolkit actions you can `integrations execute` / import |
| **Custom MCP** (`source: "mcp"`) | Live remote `tools/list` — then smoke-test with `integrations execute <org>/<name> <tool>` |
| **Other custom org apps** | Guidance only (`kind: "custom"`) — actions live in project code via `defineIntegration(...).action(...)` |

For a custom MCP, the list is an inspect step: every tool the server advertises, whether or not you have wrapped it in `app.action(...)`. Smoke-test a remote tool with `integrations execute`, then author the TypeScript wrappers you need. This split will improve later; today treat MCP list as remote discovery, not a project action inventory. See the [CLI apps reference](https://docs.os.meetluno.ai/cli.md#integrations-and-secrets).

### Smoke-testing custom app actions

`luno integrations execute` runs **built-in catalog** toolkit actions and **custom MCP** remote tools. It cannot invoke `defineIntegration(...).action(...)` code in your project for non-MCP custom apps.

To smoke-test a custom (non-MCP) app action before wiring it into larger agents/workflows:

1. Author the action with `defineIntegration(...).action(...)` (or `integrations sync` + fill in the handler).
2. Connect the app credential (`luno connect <org>/<name>`).
3. Add a thin workflow that calls that one action.
4. Deploy, then run it for real:

```bash theme={null}
luno deploy
luno workflows run <workflow-slug> --input '{...}' --wait
```

For MCP custom apps, connect, list tools, then smoke-test a remote tool with the same command family as catalog apps:

```bash theme={null}
luno connect <org>/<name> --print-url
luno integrations actions list <org>/<name>
luno integrations execute <org>/<name> <tool> --input '{}'
```

Then wrap the tools you need in `app.action(...)` and attach them (or the whole app) to an agent.

To remove a custom org app (and all of its credentials), use:

```bash theme={null}
luno integrations delete <org>/<name>
```

Official catalog apps cannot be deleted. In the web dashboard, open **Connect an app**, select the custom app, and click **Delete**.

## Example requests

Ask your coding agent which API or MCP server you want to reach. It should register a connectable app when needed, define the app in code, write actions, and attach them.

> "Connect to our internal billing API and add actions to look up and refund an invoice."

> "Attach the DeepWiki MCP server to the research agent so it can answer questions about any GitHub repo."

## Define a custom app (required for credentialed integrations)

For any new HTTP integration that needs a secret or connected account, create an app wrapper and define actions from it. Credentials belong on the app; actions are created with `app.action()`.

```ts src/integrations/acme/integration.ts theme={null}
import { defineIntegration } from "@meetluno/luno/integration";
import { z } from "zod";

// Prefer `luno integrations sync my-org/acme` after create — it generates this file.
export const acme = defineIntegration({
  slug: "my-org/acme", // full org app id from `integrations create` / `integrations list`
  auth: "api_key",
  credential: {
    apiKey: z.string(),
  },
});
```

```ts src/actions/create-acme-ticket.ts theme={null}
import { z } from "zod";
import { acme } from "../integrations/acme/integration";

export const createAcmeTicket = acme.action({
  slug: "create-acme-ticket",
  name: "Create Acme ticket",
  description: "Open a ticket in Acme",
  input: z.object({ title: z.string(), body: z.string() }),
  output: z.object({ id: z.string() }),
  async run(input, credentials) {
    return createTicket({
      apiKey: credentials["my-org/acme"].apiKey,
      title: input.title,
      body: input.body,
    });
  },
});
```

The app `slug` must match the catalog/credential key from `integrations create` (the full `{org}/{name}` id). Official catalog apps can still support self-hosted instances when their credential shape includes a base URL or similar field.

> [!WARNING]
>
> **Anti-pattern:** do not create `src/credentials/foo.ts` plus a lone `defineAction` for a new API. Create `src/integrations/foo/integration.ts` with `defineIntegration` and `app.action(...)` instead.

After the app is registered and connected:

```bash theme={null}
luno connect my-org/acme
# or, when you already have the secret in the environment:
luno credentials create my-org/acme --set apiKey=@env:ACME_API_KEY --scope org
```

See [using credentials in code](https://docs.os.meetluno.ai/learn/credentials/use-credentials.md) for scope resolution.

## When standalone `defineCredential` is appropriate

Reserve ad-hoc `defineCredential()` for rare cases — for example a shared secret used by unrelated actions that are not one service, or an MCP `auth` helper. It is not the default path for a new integration.

```ts theme={null}
import { defineCredential } from "@meetluno/luno/credentials";
import { z } from "zod";

export const sharedWebhookSecret = defineCredential({
  key: "shared-webhook-secret",
  fields: { secret: z.string() },
});
```

## Connect an MCP server

Register the MCP as an org app, **connect auth**, then inspect live tools, smoke-test with `integrations execute`, author `app.action` wrappers, and attach the **app** (or individual actions) to the agent. Credentials stay on the platform; code mirrors the app slug.

You cannot list MCP tools until the app is connected (OAuth or API key). `NO_AUTH` servers are the only exception.

```bash theme={null}
luno docs search "custom apps MCP"
luno integrations create --mcp https://mcp.example.com/mcp --auth oauth
# Use the full {org}/{name} slug from the create response:
luno connect my-org/example-mcp --print-url   # required before list (unless NO_AUTH)
luno integrations actions list my-org/example-mcp     # live tools/list with stored credential
luno integrations execute my-org/example-mcp list_accounts --input '{}'
luno integrations sync my-org/example-mcp
```

`integrations actions list` on a custom MCP uses the connected credential to call the remote `tools/list`. That output is every tool the server exposes right now — it is **not** the same as listing official catalog actions, and those tools may not exist as TypeScript `app.action(...)` functions yet. Use `integrations execute` to smoke-test a remote tool, then wrap what you need. If nothing is connected yet, the CLI tells you to connect first.

```ts theme={null}
import { createLunoClient } from "@meetluno/luno/client";
import { defineIntegration } from "@meetluno/luno/integration";
import { z } from "zod";

export const exampleMcp = defineIntegration({
  slug: "my-org/example-mcp",
  auth: "oauth",
  source: "mcp",
});

const luno = createLunoClient();

// Inspect tools with `integrations actions list`, then wrap what you need:
export const listAccounts = exampleMcp.action({
  slug: "list_accounts",
  name: "List accounts",
  description: "List accounts from the MCP server.",
  input: z.object({}),
  output: z.unknown(),
  async run(input) {
    // The platform resolves the connected credential and calls the remote `tools/call`;
    // `result` is the MCP CallToolResult (`content`, optional `structuredContent`).
    const { result } = await luno.tools.execute({
      app: "my-org/example-mcp",
      tool: "list_accounts",
      arguments: input,
    });
    return result;
  },
});
```

A deployed run resolves the project's credential for the app (project, then organization). A
failing tool throws with the tool's error text.

### Who can execute a tool

Running a tool has real effects (it sends the email, writes the CRM record), so seeing a credential
is not enough to use it. The rule depends on who the credential belongs to:

| Credential | Who can execute with it |
| - | - |
| **Personal** (a user connected it for themselves, e.g. their own mailbox) | Only that user: from the CLI/API, or from an agent or workflow run they started. Nobody else, org admins included. |
| **Shared** (organization or project credential, e.g. an app-level Microsoft Graph connection or the company CRM) | Anyone who can run agents and workflows in the project (`project:run`: Space Admin, Editor, Operator; org owners and admins). Viewers (read-only) cannot. |

Runs nobody started by hand (cron, webhooks, polls) have no acting user. They use shared credentials,
and a personal credential only when its owner **assigned** it in the project
(`luno credentials assignments assign …`). In a run started by a person, only that person's own
personal credentials execute.

Refusals answer `403` with a code you can branch on: `personal_credential_not_owned` (someone else's
personal credential) or `run_permission_required` (shared credential, read-only access). From the
CLI or API without `--project`, a shared credential needs `project:run` organization-wide (org owner
or admin, or a permission group); pass `--project <slug>` to use your role in that Space. Listing
tools (`integrations actions list`) still only needs to see the credential.

Attach tools either way:

```ts theme={null}
import { defineAgent } from "@meetluno/luno/agent";
import { exampleMcp, listAccounts } from "../integrations/example-mcp/integration";

// Entire app — every action registered on `exampleMcp` via `app.action(...)`
tools: [exampleMcp];

// Or pick specific actions
tools: [listAccounts];
```

Assign the app credential to the agent, then deploy.

Do **not** import `defineMcp` from `@meetluno/luno/agent` (it is not a supported authoring API).

OAuth MCP apps expose `accessToken` for use as `Authorization: Bearer` against the remote MCP URL.

## Current limitations

`luno.config.ts` has an `integrations` option, but custom HTTP integration mounting is not a shipped user-facing extension point yet. Today, use:

* `defineIntegration()` + `app.action()` for credentialed custom HTTP APIs (default).
* `luno integrations create` when the app must appear in Connect / the org catalog.
* `luno integrations delete <org>/<name>` to remove a custom org catalog app (and its credentials).
* Plain `defineAction()` for project actions that need no credentials.
* `luno integrations create --mcp` + connect + `integrations actions list` + `integrations execute` for custom MCP servers.

> [!NOTE]
>
> Search Luno OS docs (`luno docs search`) before inventing MCP integration patterns. Live tool lists fill gaps when vendor docs are thin.

## Next steps

> **[Using credentials in code](https://docs.os.meetluno.ai/learn/credentials/use-credentials.md)**
>
> Understand credential declaration, scopes, and defaults.

> **[Actions](https://docs.os.meetluno.ai/learn/actions/overview.md)**
>
> Build actions that consume your app credential.

> **[Build agents](https://docs.os.meetluno.ai/learn/agents/build-agents.md)**
>
> Attach actions as agent tools.

> **[Connect and manage apps](https://docs.os.meetluno.ai/learn/credentials/connect-credentials.md)**
>
> Store the API key or OAuth credential your code will resolve.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.