Skip to content

feat(cli): add tool calls and JSON output - #1088

Merged
helpcodeai merged 6 commits into
HelpCode-ai:mainfrom
Coding-Professional:feat/1009-cli-call-json
Oct 15, 2026
Merged

helpcodeai merged 6 commits into
HelpCode-ai:mainfrom
Coding-Professional:feat/1009-cli-call-json

Conversation

@Coding-Professional

@Coding-Professional Coding-Professional commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Developers can inspect and call connector tools from the terminal or CI without setting up another MCP client. This completes the CLI commands from #1009 after #1024 and targets /mcp/<serverId> on both Cloud and self-hosted instances.

Changes

  • Add tools describe and tools call, accepting an inline JSON object, @file.json, or stdin with --args -.
  • Prompt before destructive tools on a terminal; require --yes in non-interactive use.
  • Add --json to every command, document stdout/stderr shapes, and preserve the tool's isError result with exit code 1.
  • Default tool calls to a 300-second timeout; support validated --timeout <seconds> and report Tool call timed out after <seconds>s with exit code 4.
  • Keep the instance url consistent in login/whoami JSON, preserve base-path prefixes, and expose the MCP endpoint separately in whoami.
  • Use the SDK's automatic tool-list pagination and test both JSON and SSE responses.

Type

  • New feature
  • Documentation

Testing

Commands run from the repository root unless stated otherwise:

  • npm test -w packages/cli: 11 passed, including a real 65-second SSE tool call that exceeds the SDK's former 60-second default, custom timeout success/failure, pagination, URL consistency, and existing credential-isolation tests.
  • npm run lint -w packages/cli: passed.
  • npm run build -w packages/cli: passed.
  • npm test -w packages/backend -- --runInBand --testPathPatterns=adapters/catalog.spec.ts --silent: 7,717 passed.
  • node scripts/validate-adapters.mjs: 438 passed, 0 failed; 1,458 existing non-blocking warnings.
  • node scripts/adapter-count.mjs --check: passed (422 listed adapters, 19 keyless, 6,204 tools).
  • git diff --check: passed.
  • GitHub CI on the review-fix commit: CLI, full backend lint/typecheck/tests/build, frontend checks, and frontend e2e passed; CodeQL also passed.
  • Local Docker Compose smoke run: passed; commands and output below.
  • The earlier local docker build . passed on the 0.30.0 base. Later upstream release syncs and this CLI-only update were not followed by another source image build. The Compose smoke run used the published 0.32.1 image.

Local Docker Compose verification (15 October 2026)

Used a fresh isolated stack on ports 3300/4400 with the published 0.32.1 backend, a real PostgreSQL database, the keyless Hacker News connector, and a server-scoped per-user MCP key. The local account, connector and key were created through the same authenticated application API used by the dashboard; no database fixtures or fake MCP handler were used. Generated local secrets were kept in .git/cli-review.env and were not committed.

Image: helpcodeai/anythingmcp@sha256:a0ef922cd0ef779fd8a2da289400bd20a1da55416c95ff30e4e0cba5cbdc6ea0

# cli-review.env contains generated JWT_SECRET, ENCRYPTION_KEY,
# POSTGRES_PASSWORD, FRONTEND_PORT=3300 and BACKEND_PORT=4400.
docker compose --env-file .git/cli-review.env -p amcp-cli-review -f docker-compose.quickstart.yml up -d
# postgres: healthy; app: running on localhost:3300 and localhost:4400

The runner supplied the key and JSON stdin directly to the subprocess, never as command arguments. Inline, file and stdin calls all returned the real Hacker News item 1. The transcript also covers usage and authentication errors. The bad-key command temporarily used an intentionally invalid key.

CLI commands and redacted output
Local Compose instance: http://localhost:4400
Registered a local test account via the dashboard API (credentials redacted).
Installed the keyless Hacker News connector and assigned it to the default MCP server.
Created a scoped per-user MCP API key via the dashboard API (key redacted).
AMCP_URL=http://localhost:4400; AMCP_SERVER=cmv9axxz500052pp31jgoazx6; AMCP_API_KEY=[REDACTED]
$ node packages/cli/dist/index.js login --key-stdin --json
# stdin: [REDACTED key]
{"loggedIn":true,"profile":"default","url":"http://localhost:4400","server":"cmv9axxz500052pp31jgoazx6"}
exit 0
$ node packages/cli/dist/index.js tools ls --filter hackernews_get_item
hackernews_get_item	read-only	Fetch a single item (story, comment, job, poll, pollopt) by ID. Returns the full payload including `type`, `by`, `time`, `title`, `url`, `text`, `score`, `kids` (child comment IDs), `parent`.
exit 0
$ node packages/cli/dist/index.js tools describe hackernews_get_item --json
{"tool":{"name":"hackernews_get_item","description":"Fetch a single item (story, comment, job, poll, pollopt) by ID. Returns the full payload including `type`, `by`, `time`, `title`, `url`, `text`, `score`, `kids` (child comment IDs), `parent`.","inputSchema":{"type":"object","properties":{"id":{"type":"number","description":"HN item ID."}},"required":["id"],"$schema":"https://json-schema.org/draft/2020-12/schema"},"annotations":{"title":"Hackernews get item","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true}}}
exit 0
$ node packages/cli/dist/index.js tools call hackernews_get_item --args '{"id":1}' --timeout 30 --json
{"tool":"hackernews_get_item","result":{"content":[{"type":"text","text":"{\n  \"by\": \"pg\",\n  \"descendants\": 3,\n  \"id\": 1,\n  \"kids\": [\n    15,\n    487171,\n    234509,\n    82729\n  ],\n  \"score\": 57,\n  \"time\": 1160418111,\n  \"title\": \"Y Combinator\",\n  \"type\": \"story\",\n  \"url\": \"http://ycombinator.com\"\n}"}]}}
exit 0
$ node packages/cli/dist/index.js tools call hackernews_get_item --args @.git/cli-review-args.json --json
{"tool":"hackernews_get_item","result":{"content":[{"type":"text","text":"{\n  \"by\": \"pg\",\n  \"descendants\": 3,\n  \"id\": 1,\n  \"kids\": [\n    15,\n    487171,\n    234509,\n    82729\n  ],\n  \"score\": 57,\n  \"time\": 1160418111,\n  \"title\": \"Y Combinator\",\n  \"type\": \"story\",\n  \"url\": \"http://ycombinator.com\"\n}"}]}}
exit 0
$ node packages/cli/dist/index.js tools call hackernews_get_item --args - --json
# stdin: {"id":1}
{"tool":"hackernews_get_item","result":{"content":[{"type":"text","text":"{\n  \"by\": \"pg\",\n  \"descendants\": 3,\n  \"id\": 1,\n  \"kids\": [\n    15,\n    487171,\n    234509,\n    82729\n  ],\n  \"score\": 57,\n  \"time\": 1160418111,\n  \"title\": \"Y Combinator\",\n  \"type\": \"story\",\n  \"url\": \"http://ycombinator.com\"\n}"}]}}
exit 0
$ node packages/cli/dist/index.js tools call hackernews_get_item --args '{broken' --json
{"error":"--args must be a JSON object","exitCode":2}
exit 2
$ node packages/cli/dist/index.js tools ls --json
{"error":"Authentication failed (401/403)","exitCode":3}
exit 3
$ node packages/cli/dist/index.js logout --json
{"loggedOut":true,"profile":"default"}
exit 0
Revoked the local test MCP key.

The test key was revoked and the stack was stopped afterward:

docker compose --env-file .git/cli-review.env -p amcp-cli-review -f docker-compose.quickstart.yml down
# app and postgres stopped/removed; test network removed

Cloud live verification was not run because Cloud test credentials are unavailable. No Cloud-specific code or permissions were changed.

Checklist

  • Followed the existing style and reviewed the changes.
  • Added regression tests and ran the CLI locally against a real self-hosted instance.
  • Updated user-facing documentation.
  • Synced the branch with upstream main.

Related Issues

Relates to #1009. Follows #1024.

@Coding-Professional
Coding-Professional marked this pull request as ready for review October 14, 2026 08:46
Copilot AI balanced review requested due to automatic review settings October 14, 2026 08:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@helpcodeai helpcodeai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @Coding-Professional, this is in good shape. I ran npm test, lint and build in packages/cli (10/10 pass). I also pointed the CLI at a fake server wired the way our backend is (stateless createMcpHandler, both SSE and JSON response modes). Inline/file/stdin --args, isError on stdout with --json and on stderr in text mode, input validation errors, non-text content, exit 3 on a bad key, and the destructive prompt on a real TTY (y, n, empty answer, Ctrl-D) all did what the README says. The key never showed up in stdout or stderr. Copilot didn't actually review this one (quota), so there's nothing from it to address.

Two things before merge:

  1. client.callTool in src/index.ts:253 runs with the SDK's default 60s request timeout. I tried a tool that takes 65s and got exit 4 with "Could not connect to the MCP server or complete the request" after 60s. That sends people looking at the network when the call was just slow, and our database engine alone allows 60s per statement plus connect time, so a slow sap_query in CI can hit it. Please pass a longer timeout (a --timeout <seconds> flag with a default around 300 is fine) and catch the SDK's REQUEST_TIMEOUT error with its own message, something like "Tool call timed out after 300s", still exit 4.
  2. The issue's acceptance criteria ask for one run against a local docker compose up instance. That needs no credentials, just a key you create in the UI. Please paste the commands and output (key redacted) into the PR description.

Smaller stuff, fine here or in a follow-up:

  • login --json puts the origin in url (index.ts:179) but whoami --json puts the full /mcp/<id> URL in url (index.ts:195). I'd keep the instance URL in url for both and add the endpoint as a separate field if you want it, before scripts start relying on it.
  • The README lists the JSON shapes for stdout but not the error object on stderr ({"error": ..., "exitCode": ...}). Worth one line.
  • The fake server in cli.test.ts only answers with application/json. Our backend defaults to SSE, so one SSE case would be good to have.
  • listClientTools in mcp.ts: with client 2.2, client.listTools() without a cursor already walks every page and returns no nextCursor, so the loop never gets past the first pass. return (await client.listTools()).tools is enough.
  • Not from this PR: a refused connection (e.g. nothing on the port) still prints the generic message, because the error has no cause.code there (I get TypeError: fetch failed with a cause that carries no code), so the ECONNREFUSED branch from part 1 never matches.

The branch is also behind main, so it needs an update before it can merge.

@Coding-Professional

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review and testing. I'm addressing the timeout handling and the smaller improvements, and I'll add the local Docker Compose commands and redacted output to the PR description. I'll push the updates to this PR once verification is complete and keep the branch synced with main.

@helpcodeai helpcodeai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @Coding-Professional, both points are done.

  • --timeout defaults to 300s, validates its input and fails with "Tool call timed out after Ns" and exit 4.
  • The Compose run against 0.32.1 in the description is exactly what the issue asked for.

I re-ran it locally: npm test 11/11, including the slow SSE call and the paginated tool list, and npm run build is clean. Merging.

@helpcodeai
helpcodeai merged commit 86da6a7 into HelpCode-ai:main Oct 15, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants