Skip to content

feat(mcp-cli): standalone CLI with API key & OAuth over Streamable HTTP#222

Closed
abhinavmathur-atlan wants to merge 26 commits into
mainfrom
feat/mcp-cli
Closed

feat(mcp-cli): standalone CLI with API key & OAuth over Streamable HTTP#222
abhinavmathur-atlan wants to merge 26 commits into
mainfrom
feat/mcp-cli

Conversation

@abhinavmathur-atlan
Copy link
Copy Markdown
Collaborator

@abhinavmathur-atlan abhinavmathur-atlan commented Apr 27, 2026

Summary

  • Adds mcp-cli/ — a standalone atlan CLI for calling Atlan MCP tools directly from the terminal (no IDE, no agent required)
  • Persistent credential storage via OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service) with file fallback
  • OAuth PKCE flow via mcp.atlan.com proxy with automatic token refresh
  • API key mode against any Atlan tenant
  • 30 MCP tools as typed subcommands with schema-aware argument parsing

Auth

# One-time login — interactive chooser (OAuth or API key)
atlan login

# Or non-interactive
atlan login --oauth                                            # browser login
atlan login --api-key sk-xxx --tenant https://demo.atlan.com  # API key

# Daily use — no flags needed
atlan semantic_search_tool --user-query "PII tables"
atlan list-tools
atlan status
atlan logout
Storage Contents
~/.atlan/config.json Auth mode + tenant URL (no secrets)
OS keychain atlan-mcp Access token, refresh token, or API key
~/.atlan/credentials.json Fallback when keychain unavailable (CI/headless)

Installation

uv tool install /path/to/agent-toolkit/mcp-cli
# Future: uv tool install atlan-cli

Exit codes

Code Meaning
0 Success
1 Tool returned an error
2 Not authenticated — run atlan login
3 Config or invocation error

Test plan

  • atlan login interactive chooser — OAuth and API key paths
  • atlan login --api-key ... --tenant ... non-interactive
  • atlan status — shows mode, tenant, token expiry
  • atlan logout — wipes keyring + config
  • Access token auto-refresh on expiry (refresh grant to mcp.atlan.com)
  • Refresh token revocation → exit 2, credentials wiped
  • All 30 tools execute successfully against atlan-demo (OAuth) and projectred (API key)
  • --json flag emits raw JSON to stdout, logs to stderr
  • Keychain unavailable → falls back to ~/.atlan/credentials.json
  • Schema-aware arg parsing — pure string|null params bypass JSON parse

🤖 Generated with Claude Code

Adds atlan_cli.py and SKILL.md to mcp-cli/ — a standalone CLI for
calling all 30 Atlan MCP tools directly from the terminal.

Auth modes (controlled via env vars):
- API key: ATLAN_BASE_URL + ATLAN_API_KEY → /mcp/api-key with Bearer auth
- OAuth (PKCE): ATLAN_BASE_URL only → /mcp with browser-based login

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
abhinavmathur-atlan and others added 22 commits April 27, 2026 20:47
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- sys.argv.remove("--oauth") prevents cyclopts from seeing the flag
  as an unknown command (was set inline before, still hit cyclopts)
- README: fix incorrect generation URL reference (was mcp.atlan.com/mcp,
  which is an internal proxy); update regenerating command to use
  tenant URL; add --oauth and ATLAN_AUTH=oauth auth-override docs

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…/dotenv

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…eeStore

Eliminates browser re-auth on every CLI invocation. Tokens are stored
per-server-URL in ~/.atlan/mcp-tokens using key_value FileTreeStore
(already a transitive dep of fastmcp). Also fixes README uv run syntax.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…en persistence

FileTreeStore used keys directly as filesystem paths, which broke when keys
were URLs (e.g. https:/tenant/mcp created nested directories that didn't exist).
_JsonFileStore hashes the collection name for the filename and uses dict keys
for key lookup — safe for any key string including URLs.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Prerequisites: uv run handles PEP 723 deps automatically, no pip install needed
- Auth modes: note that OAuth tokens are cached in ~/.atlan/mcp-tokens/
- Regenerating: replace fragile line-number reference with structural marker

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add pyproject.toml with setuptools build backend and atlan script entry
  point so users can install with `uv tool install .` instead of invoking
  via `uv run python3 atlan_cli.py`
- Remove the `call-tool` sub-app; all tool commands now live directly on
  the root app so invocation is `atlan semantic_search_tool` instead of
  `atlan call-tool semantic_search_tool`
- Add main() entry point function wired to the pyproject.toml [project.scripts]
- Remove PEP 723 inline script metadata (superseded by pyproject.toml)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Move auth resolution from module-level to a lazy _resolve_auth() function
called only when a command actually connects to the server. Also fixes
--oauth handling by stripping it in main() and setting ATLAN_AUTH=oauth.

Also fix pyproject.toml build-backend to setuptools.build_meta (the legacy
backend path was removed in newer setuptools versions).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Fix critical bug: Client(*_resolve_auth()) was mapping the OAuth
  handler to the `name` param (2nd positional arg), not `auth`.
  Replace all call sites with _client() helper that explicitly passes
  auth=auth as a keyword argument.
- Load .env from cwd before script-dir so installed tool respects
  a .env in the working directory.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Reflect new uv tool install workflow, direct tool invocation without
the call-tool prefix, and OAuth token caching behaviour.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Captures the agreed direction from the meeting with Ankit and
Hrushikesh: persistent login state, hardcoded proxy URL, ~/.atlan
config dir, PyPI distribution modeled on pyatlan, agent-friendly
diagnostics, and a phased rollout aligned with the Activate demo.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…sign

Incorporates findings from agent-toolkit-internal:mcp_proxy: the proxy
already supports refresh_token grant and derives tenant from the JWT
issuer claim, so the CLI can store only the refresh token and never
needs to know the tenant URL.

- Detailed file layout (src/atlan_cli/auth, commands, transport)
- Auth resolver pseudo-code with stale-cache wipe invariants
- Refresh-token flow against mcp.atlan.com/oauth/token
- Decision: keep cyclopts, add rich/questionary for prettier output
- Phased breakdown with effort estimates per item
- GitHub Actions publish workflow modeled on pyatlan-publish.yaml
- Test plan, risks, references

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…hing

- atlan login / logout / status commands with OS keyring storage
  (~/.atlan/config.json + keyring with ~/.atlan/credentials.json fallback)
- OAuth refresh-token grant via mcp.atlan.com proxy; stale-cache wipe on
  failure; exit 2 so callers can detect auth-required vs tool errors
- _resolve_auth() hierarchy: per-call override → stored config → refresh →
  exit 2; legacy ATLAN_BASE_URL env-var path preserved for backwards compat
- Global flags --oauth / --api-key / --tenant / --json stripped in main()
  before cyclopts sees argv; --json routes all output to stdout, logs to stderr
- Exit codes: 0 success, 1 tool error, 2 auth required, 3 config/invocation
- Rich UI: list-tools as Table, status/login as Panels, questionary chooser
- Package renamed atlan-cli (was atlan-mcp-cli); hatchling build backend;
  dynamic version from __version__; dependency version ranges pinned
- .github/workflows/mcp-cli-publish.yaml: release tag → uv build + uv publish
- API key validation switched from /api/meta/whoami to /api/meta/types/typedefs
  (whoami returns 500 on some tenants; typedefs is more reliable)
- mcp-cli/.gitignore: excludes build artifacts, .env, .venv, uv.lock,
  .fastmcp-cache/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ways opens browser

Without this, .fastmcp-cache/ survived logout and FastMCP reused cached
tokens silently, making tenant switching impossible without manual cache deletion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove dead _KeyringStore and _CapturingStore classes (never used in practice;
  flat keyring helpers and direct token extraction cover all paths)
- Add _maybe_json() helper: safe json.loads with fallback for non-JSON strings,
  replacing bare json.loads() which crashed on free-form text
- Make argument parsing schema-aware: pure string|null params (title, message,
  announcement_type, guid, qualified_name, asset_type, name_filter, group_id,
  default_schema, connection_qualified_name, sort_by, glossary_qualified_name,
  include_attributes in traverse/get_asset) now pass through without JSON decoding;
  object/array/integer params continue to use _maybe_json()
- Fix _wipe_credentials() to reset cached _resolved auth so logout is effective
  in long-lived processes
- Move Panel import to top-level; remove three inline from-imports
- Remove # Parse JSON parameters comments throughout generated section
- Improve README: namespace-type enum values, exit codes table, cross-platform
  keychain details, write-tool propose/execute workflow, tool category table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
questionary was not installed (not in deps), causing atlan login to silently
skip the chooser and go straight to OAuth. Now uses rich.prompt (already a
required dep) for the method selector and masked API key input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getpass.getpass() reads char-by-char in raw terminal mode and stalls
visibly when pasting long JWT tokens (1000+ chars). input() uses the
terminal's own line-edit mode and handles pastes instantly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- workflow: allow workflow_dispatch (was blocked by tags-only if condition)
- gitignore: add plugin build artifact patterns (*.tar.gz, *.zip)
- README: add PyPI install snippet, document interactive login flow with example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three F541 bare f-strings and one formatting inconsistency flagged by
pre-commit ruff hooks; auto-fixed with ruff --fix + ruff format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds id-token: write permission and removes PYPI_API_TOKEN env var.
OIDC trusted publisher is more secure — no long-lived secret needed.
Configure the pending publisher at pypi.org with:
  project: atlan-cli, owner: atlanhq, repo: agent-toolkit,
  workflow: mcp-cli-publish.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Triggers on PR merge to main with 'release-cli' label (mirrors 'release'
  label used by mcp-server-release.yml, separate to avoid double-trigger)
- Reads __version__ from mcp-cli/atlan_cli.py
- Creates tag mcp-cli-vX.Y.Z, GitHub Release with auto-changelog
- Publishes to PyPI via OIDC trusted publisher (no API token secret)
- workflow_dispatch still works for manual runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
abhinavmathur-atlan and others added 3 commits April 28, 2026 18:15
- Moved uv.lock, .fastmcp-cache/, .env.* rules into root .gitignore
- Removed mcp-cli/.gitignore (redundant now)
- Removed mcp-cli/PLAN.md (implementation complete, context lives in git history)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep only plugin artifact patterns (.tar.gz, .zip) which cover real
untracked files. Remove .env.*, uv.lock, .fastmcp-cache/ — not needed
at root level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Not needed — these are stray local files, not repo concerns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@abhinavmathur-atlan
Copy link
Copy Markdown
Collaborator Author

Moving mcp-cli to agent-toolkit-internal for now. Publishing to PyPI will come in a follow-up once confirmed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant