> ## Documentation Index
> Fetch the complete documentation index at: https://keystroke.ai/docs/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}
keystroke apps search "<service or capability>"
keystroke apps 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 **Apps** / `keystroke connect` so someone can enter a secret | `keystroke apps create` (manual fields or `--openapi` / `--mcp` / `--graphql`) |
| **Code authoring**       | Declares the app credential once and builds actions that use it                      | `defineApp()` in `src/apps/`, then `app.action(...)`                           |

Code-only `defineApp` does **not** register an org catalog app. Without `apps create`, the service will not show up in the Connect dialog, and `keystroke 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 (`keystroke apps search`).
2. **Register the app** — `keystroke apps create` so it is connectable in the org.
3. **Connect** — user enters the secret in the web Apps flow (`keystroke connect <slug>` or the MCP `connect_app` link), or `keystroke credentials create` for a static key you already have in the shell.
4. **Author in code** — `defineApp` + `app.action(...)` (or `keystroke apps 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}
keystroke apps create --openapi <url> --preview
keystroke apps create --mcp <url> --preview
keystroke apps 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}
keystroke apps 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 Apps 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`, `apps sync`, and `defineApp({ slug })`. After create, prefer:

```bash theme={null}
keystroke apps sync <org>/<name>
```

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

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

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

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

### Smoke-testing custom app actions

`keystroke apps execute` runs **built-in catalog** toolkit actions and **custom MCP** remote tools. It cannot invoke `defineApp(...).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 `defineApp(...).action(...)` (or `apps sync` + fill in the handler).
2. Connect the app credential (`keystroke connect <org>/<name>`).
3. Add a thin workflow that calls that one action.
4. Deploy, then run it for real:

```bash theme={null}
keystroke deploy
keystroke 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}
keystroke connect <org>/<name> --print-url
keystroke apps actions list <org>/<name>
keystroke apps 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}
keystroke apps 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/apps/acme/app.ts theme={null}
import { defineApp } from "@keystrokehq/keystroke/app";
import { z } from "zod";

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

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

export const createAcmeTicket = acme.action({
  slug: "create-acme-ticket",
  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 `apps 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/apps/foo.ts` with `defineApp` and `app.action(...)` instead.
</Warning>

After the app is registered and connected:

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

See [using credentials in code](/docs/learn/credentials/use-credentials) 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 "@keystrokehq/keystroke/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 `apps 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}
keystroke docs search "custom apps MCP"
keystroke apps create --mcp https://mcp.example.com/mcp --auth oauth
# Use the full {org}/{name} slug from the create response:
keystroke connect my-org/example-mcp --print-url   # required before list (unless NO_AUTH)
keystroke apps actions list my-org/example-mcp     # live tools/list with stored credential
keystroke apps execute my-org/example-mcp list_accounts --input '{}'
keystroke apps sync my-org/example-mcp
```

`apps 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 `apps 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 { defineApp } from "@keystrokehq/keystroke/app";
import { z } from "zod";

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

// Inspect tools with `apps 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, credentials) {
    // Call the remote MCP tool with credentials["my-org/example-mcp"].accessToken
    // (Bearer) — see vendor docs for the exact call shape.
    return {};
  },
});
```

Attach tools either way:

```ts theme={null}
import { defineAgent } from "@keystrokehq/keystroke/agent";
import { exampleMcp, listAccounts } from "../apps/example-mcp/app";

// 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 `@keystrokehq/keystroke/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

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

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

<Note>
  Search Keystroke docs (`keystroke docs search`) before inventing MCP integration patterns. Live tool lists fill gaps when vendor docs are thin.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Using credentials in code" href="/docs/learn/credentials/use-credentials">
    Understand credential declaration, scopes, and defaults.
  </Card>

  <Card title="Actions" href="/docs/learn/actions/overview">
    Build actions that consume your app credential.
  </Card>

  <Card title="Build agents" href="/docs/learn/agents/build-agents">
    Attach actions as agent tools.
  </Card>

  <Card title="Connect and manage apps" href="/docs/learn/credentials/connect-credentials">
    Store the API key or OAuth credential your code will resolve.
  </Card>
</CardGroup>
