Metadata-Version: 2.4
Name: tableau-hosted-mcp
Version: 0.1.0a1
Summary: Python SDK for Tableau Hosted MCP
Author: Mohamed Setit
License: MIT
Keywords: tableau,mcp,fastmcp,oauth,cimd,ai
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastmcp>=3.4.2
Requires-Dist: typer>=0.16
Requires-Dist: rich>=14.0
Requires-Dist: pydantic>=2.11
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: platformdirs>=4.3
Requires-Dist: py-key-value-aio[disk]>=0.4
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: build; extra == "dev"
Provides-Extra: playground
Requires-Dist: streamlit>=1.29; extra == "playground"
Requires-Dist: anthropic>=0.40; extra == "playground"
Requires-Dist: openai>=1.40; extra == "playground"
Dynamic: license-file

# tableau-hosted-mcp

Python SDK for [Tableau Hosted MCP](https://mcp.tableau.com). Handles OAuth 2.1
PKCE via a CIMD-registered public client, persists tokens locally, and exposes
both an async and sync API plus a `tableau-mcp` CLI. Ready to drop into an LLM
agent — the MCP tool schemas map 1:1 to Anthropic and OpenAI tool-use formats.

## Install

```bash
pip install tableau-hosted-mcp
```

Requires Python 3.11+.

## First run

The first call opens a browser tab to `sso.online.tableau.com`. Sign in once —
tokens are persisted to the platform's user-config directory (resolved via
[platformdirs](https://pypi.org/project/platformdirs/)) and reused on
subsequent runs.

| Platform | Default cache directory |
| --- | --- |
| macOS | `~/Library/Application Support/tableau-hosted-mcp/` |
| Linux | `~/.config/tableau-hosted-mcp/` (respects `$XDG_CONFIG_HOME`) |
| Windows | `%LOCALAPPDATA%\tableau-hosted-mcp\tableau-hosted-mcp\` |

Override with the `TABLEAU_MCP_CONFIG_DIR` env var if you want tokens on an
encrypted volume, a shared drive, or an ephemeral location. Storage is a
SQLite-backed key-value store (via `diskcache` under `py-key-value-aio`); no
native binaries, works identically on all three platforms.

The easiest way to warm up the token cache is:

```bash
tableau-mcp login
```

## Library usage (async)

```python
import asyncio
from tableau_hosted_mcp import TableauHostedClient

async def main():
    async with TableauHostedClient() as client:
        tools = await client.list_tools()
        print(f"{len(tools)} tools available")

        result = await client.call_tool("list-projects", {})
        # result is fastmcp.client.client.CallToolResult:
        #   .is_error: bool
        #   .content: list[mcp.types.ContentBlock]  (text/image/resource)
        #   .structured_content: dict | None
        #   .data: Any
        print(result.content[0].text)

asyncio.run(main())
```

Also available: `list_resources()`, `list_prompts()`, and `.fastmcp` for direct
access to the underlying `fastmcp.Client` if you need something the wrapper
doesn't expose.

## Library usage (sync)

For scripts, notebooks, Django management commands, or anything without an
event loop:

```python
from tableau_hosted_mcp import TableauHostedClientSync

with TableauHostedClientSync() as client:
    tools = client.list_tools()
    result = client.call_tool("list-projects", {})
```

The sync facade runs a dedicated background thread with its own asyncio loop.
The connection persists across method calls — you pay OAuth cost once, not per
call. Do not share a single instance across threads.

## CLI

```
tableau-mcp login              # run the OAuth flow, persist tokens
tableau-mcp logout             # clear stored tokens (recovery for stale-cache errors)
tableau-mcp doctor             # diagnose config + connectivity (CIMD reachable, MCP
                               #   reachable, live list_tools round-trip)
tableau-mcp list-tools         # list available MCP tools
tableau-mcp call-tool NAME [--arg key=value ...] [--json '{"k": "v"}']
                               # invoke a tool, print JSON result
tableau-mcp playground         # open the Streamlit UI playground (requires [playground] extra)
tableau-mcp --version
tableau-mcp --log-level DEBUG  # verbose httpx + MCP protocol logs
```

Every subcommand honours the same env-var-driven config as the library API
(see below).

## Configuration

`Settings` reads from four sources, highest precedence first:

1. Constructor kwargs — `Settings(server_url=..., verify_ssl=False)`
2. Environment variables (prefix `TABLEAU_MCP_`):
   - `TABLEAU_MCP_SERVER_URL` (default `https://mcp.tableau.com`)
   - `TABLEAU_MCP_CIMD_URL` (default your CIMD `client.json` URL)
   - `TABLEAU_MCP_CONFIG_DIR`
   - `TABLEAU_MCP_LOG_LEVEL`
   - `TABLEAU_MCP_VERIFY_SSL`
   - `TABLEAU_MCP_TIMEOUT`
3. `.env` file in the current working directory
4. Built-in defaults

`Settings.from_file(path, **overrides)` also loads a JSON config file.

## Exceptions

```python
from tableau_hosted_mcp import (
    TableauHostedMCPError,   # base — catch this to handle any SDK failure
    ConfigurationError,      # invalid settings
    AuthenticationError,     # OAuth failed even after auto-retry with cleared tokens
    ConnectionError,         # transport failure reaching the MCP server (httpx-level)
)
```

`connect()` automatically retries **once** on the stale-cache OAuth 500 pattern
by clearing the token store and re-authenticating. If that retry also fails,
you get an `AuthenticationError`. That means the manual `logout && login`
recovery step is normally invisible to callers.

## Using it inside an LLM agent

The MCP tool schemas exposed by `client.list_tools()` are plain JSON Schema
objects (`mcp.types.Tool.inputSchema`). They map 1:1 to the tool-use formats
used by:

- **Anthropic API** — `input_schema` field on tool specs
- **OpenAI API** — `parameters` field on function specs
- **Any framework built on those primitives** (LangChain, LlamaIndex, etc.)

The pattern for a manual agent loop is:

1. `tools = await client.list_tools()` — the 24 Tableau MCP tools
2. Convert each `Tool` to your LLM's tool spec (change the wrapper keys)
3. Pass the user's question + the tool specs to the LLM
4. LLM responds with tool_use blocks
5. For each tool_use, `await client.call_tool(name, arguments)`
6. Feed the tool results back as the next user message
7. Loop until the LLM returns a final text answer

See `examples/04_claude_agent.py` for a complete working implementation.

## Examples

The [`examples/`](./examples) directory contains runnable scripts:

| File | What it shows |
| --- | --- |
| [`01_quickstart_async.py`](examples/01_quickstart_async.py) | Connect, list tools, call `list-projects`. |
| [`02_quickstart_sync.py`](examples/02_quickstart_sync.py) | Same flow with the sync facade — no `asyncio` in your code. |
| [`03_data_pipeline.py`](examples/03_data_pipeline.py) | Chain `search-content` → `get-workbook` → `list-views` → `get-view-data`. Pure-SDK, no LLM. |
| [`04_claude_agent.py`](examples/04_claude_agent.py) | Wire the MCP tools into an Anthropic Claude agent loop. |
| [`05_openai_compat_agent.py`](examples/05_openai_compat_agent.py) | Same loop against any OpenAI-compatible endpoint (OpenAI, Groq, Together, Ollama, LM Studio, …). |

Run any of them from the repo root:

```bash
python examples/01_quickstart_async.py
```

The Claude example needs `pip install anthropic` and
`export ANTHROPIC_API_KEY=sk-ant-...`.

The OpenAI-compat example needs `pip install openai` and `OPENAI_API_KEY`.
Point at a non-OpenAI endpoint via `OPENAI_BASE_URL`, e.g.

```bash
export OPENAI_BASE_URL=http://localhost:11434/v1  # Ollama
export OPENAI_MODEL=qwen2.5-coder:14b
export OPENAI_API_KEY=ollama                       # placeholder, endpoint ignores it
python examples/05_openai_compat_agent.py
```

## Playground

A Streamlit UI for exploring the SDK interactively — pick tools from a
dropdown, run them with an auto-generated form, chat with an LLM that has the
tools wired up, and inspect diagnostics + settings.

Install and launch:

```bash
pip install "tableau-hosted-mcp[playground]"
tableau-mcp playground
```

Four tabs:

- **Tool Explorer** — every MCP tool's JSON schema becomes a form; run it and
  see JSON / images / structured content rendered in place.
- **Agent Chat** — switch between Anthropic (Claude) and OpenAI-compatible
  endpoints, watch each tool call unfold in a collapsible expander.
- **Diagnostics** — live version of `tableau-mcp doctor` plus token cache
  contents.
- **Settings** — resolved effective values, which env vars are present, and
  the `.env` file's contents.

Set `ANTHROPIC_API_KEY` and/or `OPENAI_API_KEY` (and `OPENAI_BASE_URL` for
non-OpenAI endpoints) before launching if you want the chat tab.

## Troubleshooting

**OAuth 500 from `sso.online.tableau.com`** — the SDK auto-retries after
clearing the token cache; you should rarely see this bubble up. If it persists,
run `tableau-mcp logout` and try again, or open your CIMD document
(`TABLEAU_MCP_CIMD_URL`) in a browser to confirm it's serving valid JSON.

**"Client failed to connect" with a stack trace** — run `tableau-mcp doctor`.
It probes CIMD reachability, MCP endpoint reachability, and full auth
round-trip, and points at whichever step failed.

**Tokens on the wrong tenant** — `tableau-mcp logout` clears the stored token
for the current `TABLEAU_MCP_SERVER_URL` value; run it before switching
tenants.

## License

MIT. See [LICENSE](./LICENSE).
