Skip to content

feat(mcp): add 'apify mcp install <client>' command#1145

Open
MQ37 wants to merge 20 commits into
masterfrom
feat/mcp-install
Open

feat(mcp): add 'apify mcp install <client>' command#1145
MQ37 wants to merge 20 commits into
masterfrom
feat/mcp-install

Conversation

@MQ37
Copy link
Copy Markdown
Contributor

@MQ37 MQ37 commented May 18, 2026

Context

Implements the "configuration only" half of apify/ai-team#147 — a new apify mcp install <client> command that configures the Apify MCP server in six AI clients (Claude Code, Cursor, VS Code, Codex CLI, Kiro, Antigravity).

Closes apify/ai-team#175
Part of apify/ai-team#147

Solution

Per-client handler dispatched by the client argument, writing the documented config shape to each client's user-global location, or shelling out to the client's own CLI (claude mcp add --scope user, codex mcp add). Token resolves from --tokenAPIFY_TOKEN env → stored apify login. JSON files merge the apify entry without clobbering other entries; an existing entry triggers a --yes/prompt overwrite gate (matches the YesFlag() convention used by actors rm, builds rm, etc.).

Worth your attention

  • Token never reaches the terminal. claude mcp add echoes the Authorization header on success, so we use stdio: ['ignore', 'ignore', 'inherit'] to discard child stdout and keep live errors. The overwrite-confirm prompt no longer prints the existing entry either. Two expect(stderr).not.toContain(TEST_TOKEN) tests guard against regression.
  • Claude Desktop intentionally not included — its MCP support currently relies on the mcp-remote local stdio bridge, which is on a deprecation track as Claude Desktop adds native remote-MCP support via Connectors. Easy to add back later.
  • claude-code passes --scope user so the server lands in user-wide config, matching every other client's default user-global location.
  • Codex uses bearer_token_env_var = "APIFY_TOKEN" — codex's runtime rejects literal bearer_token (Config schema exposes unsupported MCP bearer_token field openai/codex#19275), so env var indirection is the only HTTP-transport auth it accepts. The next-steps tell the user to export APIFY_TOKEN=<your-token> in their shell rc.
  • userHomeDir helper extracted to src/lib/utils.ts and reused from cli-management/install.ts (removes the second copy of the same idiom).

Follow-up

  • apify/ai-team#176 — built-in agent (apify <prompt>) bundling mcpc + OpenRouter Actor.

Configures the Apify MCP server in six AI clients (claude-code, cursor, vscode, codex, kiro, antigravity) by writing the client's canonical config or shelling out to its own 'mcp add' CLI.
@MQ37 MQ37 requested review from szaganek and vladfrangu as code owners May 18, 2026 07:17
@github-actions github-actions Bot added t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics. labels May 18, 2026
@vladfrangu
Copy link
Copy Markdown
Member

VS Code, and possibly it's forks (I only have Cursor that I checked), have a CLI interface for adding MCP servers

Model Context Protocol
  --add-mcp <json> Adds a Model Context Protocol server definition to the user profile. Accepts JSON input in the form '{"name":"server-name","command":...}'

let's avoid editing files wherever possible. And if we cannot avoid editing files, we must preserve everything in them at all costs (not necessarily indentation between tabs and spaces but definitely comments [so read the file via I think JSON5])

@vladfrangu
Copy link
Copy Markdown
Member

Let's also add in vscode-insiders please (identical to vscode except binary is code-insiders)

Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
Comment thread docs/reference.md Outdated
@vladfrangu
Copy link
Copy Markdown
Member

Don't forget reference docs changes need to be applied in code, not the markdown file!

MQ37 added 2 commits May 18, 2026 11:18
Switch the vscode handler to shell out to 'code --add-mcp <json>' so VS Code itself owns the config write (preserves JSONC comments, format, and overwrite semantics). Add vscode-insiders as a sibling client using 'code-insiders --add-mcp'. Other file-edit clients (cursor, kiro, antigravity) keep their current handlers — their CLIs either don't expose '--add-mcp' (cursor's wrapper double-evals args breaking JSON quoting) or their --add-mcp behavior isn't suitable for our use case.
After a successful install, a link to the third-party client's MCP docs is noise. The user just configured the server — they don't need to look up how the client handles MCP.
@MQ37
Copy link
Copy Markdown
Contributor Author

MQ37 commented May 18, 2026

@vladfrangu Good point! I verified and the CLI command approach works only for VSCode but not for the forks - so I am using the --add-mpc for the vscode and vscode-insiders client. Also lets use https://www.npmjs.com/package/jsonc-parser for manipulating the files so we can actually preserve the comments.

@vladfrangu
Copy link
Copy Markdown
Member

Really, it doesn't work for cursor? They DO document the flag, does it just not register correctly? 😢

@MQ37
Copy link
Copy Markdown
Contributor Author

MQ37 commented May 18, 2026

@vladfrangu For Cursor I could not pass the JSON as they have some bug in the CLI wrapper bash script. And for antigravity and kiro it did not work - the commands return "Added MCP servers: apify" but the server is not actually written to the kiro or antigravity config and thus is not available. I think they just left it there as they are forks of vscode and never actually implemented this properly.

MQ37 added 3 commits May 18, 2026 11:58
Switch file-edit clients (cursor, kiro, antigravity) from JSON.parse + JSON.stringify to jsonc-parser's surgical modify()/applyEdits() API. The library patches the source text in place: comments, trailing commas, indentation, and unrelated keys all survive untouched. Adds a regression test that pre-seeds a hand-edited JSONC file with comments and asserts they survive a re-install.
Keep only Server URL, Auth, and Config path. The per-client step lists were noise — users already know how to restart their editor.
"Use these commands to configure the Apify MCP server in your AI client." — drop "favorite", drop parenthetical client list from the section intro. Colon-separate client names in the index description, drop parenthetical '(or merges)' and '(forwarded as…)' from the install command text.
@MQ37
Copy link
Copy Markdown
Contributor Author

MQ37 commented May 18, 2026

@szaganek all you suggestions applied, thank you!

@patrikbraborec
Copy link
Copy Markdown
Contributor

Hi, @l2ysho is going to work on #1115. Maybe we will need to change implementation based on it?

@MQ37
Copy link
Copy Markdown
Contributor Author

MQ37 commented May 19, 2026

@patrikbraborec in the current implementation we are using the getLocalUserInfo helper func, that should be properly updated once we move to the keyring, so I think there are no changes needed in this PR regarding getting the local token.

@vladfrangu what is your take on this?

Copy link
Copy Markdown
Member

@vladfrangu vladfrangu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nits

Comment thread src/commands/mcp/install.ts
Comment thread src/lib/mcp/clients.ts Outdated
printResult({
clientLabel: 'Codex CLI',
serverUrl: url,
authDescription: `Bearer token from APIFY_TOKEN environment variable`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have no way to tell MCP clients or w/e they need to run a cli command to get the token?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean? Like we should try to detect client installed on the system? I would honestly no do that, but it is my personal preference - I like explicit control about what I install and what does it do.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nono, to tell these mcp clients like codex "hey to get this token please run apify auth token"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on openai docs this is the only way https://developers.openai.com/codex/mcp#streamable-http-servers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is so stupid...can we propose that to them? Maybe even clanker pr it 😂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can try but from my experience with this approach we never actually ship this feature as pushing PR there will be super painful - but maybe they are fast and responsive 😄 I would honestly merge this is we want to ever ship this and then we can negotiate with them and iterate on this Apify CLI feature.

Comment thread test/local/commands/mcp/install.test.ts Outdated
MQ37 and others added 2 commits May 19, 2026 22:28
Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
Per PR review (#1145): hoist the inline 'originalJsonc' literal out of
install.test.ts into test/__setup__/fixtures/mcp-install-fixtures.ts so
similar fixtures land alongside the existing mock-openapi-spec.
@MQ37 MQ37 requested a review from vladfrangu May 20, 2026 08:07
@MQ37
Copy link
Copy Markdown
Contributor Author

MQ37 commented May 20, 2026

@vladfrangu some tests are failing but it should be unrelated to this PR:


 FAIL  test/api/__fixtures__/commands/python/python-scrapy-template-works.test.ts > [python] [api] scrapy template works > should run the actor
 FAIL  test/api/__fixtures__/commands/python/python-scrapy-template-works.test.ts > [python] [api] scrapy template works > should run the actor
 FAIL  test/api/__fixtures__/commands/python/python-scrapy-template-works.test.ts > [python] [api] scrapy template works > should run the actor
 FAIL  test/api/__fixtures__/commands/python/python-scrapy-template-works.test.ts > [python] [api] scrapy template works > should run the actor

MQ37 added 8 commits May 27, 2026 12:44
useAuthSetup() already calls vitest.unstubAllEnvs() in its own afterEach
and immediately follows with rm(GLOBAL_CONFIGS_FOLDER(), { recursive,
force }). Our local afterEach was running the same unstubAllEnvs first
(vitest fires afterEach hooks LIFO), so by the time useAuthSetup's
cleanup ran, HOME was already restored to the real value and
GLOBAL_CONFIGS_FOLDER() resolved to <realHome>/.apify — wiping the
developer's auth.json, secrets.json, and telemetry on every local
run of this test file. CI users started clean so it didn't surface
there.

Drop the duplicate call; let useAuthSetup own env cleanup.
- install.ts --token help: 'apify auth token' -> 'apify login' (the
  command that stores the token; 'apify auth token' only prints it,
  and the auth.ts error message already references 'apify login').

- install.ts interactiveNote: scope --yes to JSON-merge clients
  (cursor, kiro, antigravity). For CLI-delegated clients (claude-code,
  vscode, vscode-insiders, codex), --yes is silently a no-op since
  overwrite semantics are owned by the child CLI.

- clients.ts codexHandler: codex configures bearer_token_env_var but
  never embeds the token, so users hit 401 if APIFY_TOKEN isn't in
  the environment when they launch codex. The not-found branch
  already mentioned this; the success branch did not. Add a Next:
  line on success too, and reword both to say 'in the same shell'
  rather than 'in your shell rc' (a one-off export is fine).

- printResult: new optional nextSteps field so other handlers can
  surface post-install instructions in the same block.
jju was already a dependency (used by actors/pull.ts for the same
class of JSONC round-trip). Verified that jju.update preserves
top-of-file comments, inline comments, trailing commas, indentation,
and unrelated sibling keys for every fixture the test suite asserts on.

Greenfield writes (missing/empty file) go through jju.stringify
instead of jju.update because update from a '{}' seed produces a
collapsed inline shape; stringify gives clean 2-space indented JSON.

Net result: one fewer direct dep, no behavior change at the
test-fixture level (all 13 install.test.ts cases pass).
Unrelated upstream drift picked up by 'pnpm run update-docs'. The
api command's --describe and --search help strings were updated in
#1128 but reference.md wasn't regenerated at the time. Including
here only because update-docs is monolithic and refused to re-render
just the mcp section.
Seven hand-rolled install handlers shared only two shapes: merge a JSON
config file, or shell out to the client's own CLI. Replace them with a
fileClient factory and a runCliInstall helper, derive the supported-client
list from the handler map (one source of truth), fold exec-helpers in, and
inline the single-use token resolver and test fixture path.

No behavior change: every user-facing string and exit code is preserved;
docs/reference.md is unchanged and the install test suite stays green.

Net -44 lines, -3 files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants