tableau-hosted-mcp 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tableau_hosted_mcp-0.1.0a1/LICENSE +21 -0
- tableau_hosted_mcp-0.1.0a1/PKG-INFO +260 -0
- tableau_hosted_mcp-0.1.0a1/README.md +231 -0
- tableau_hosted_mcp-0.1.0a1/pyproject.toml +74 -0
- tableau_hosted_mcp-0.1.0a1/setup.cfg +4 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/__init__.py +29 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/__init__.py +46 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/doctor.py +75 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/login.py +26 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/logout.py +31 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/playground.py +76 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/tools.py +77 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/client.py +150 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/constants.py +17 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/exceptions.py +14 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/logging.py +22 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/playground.py +626 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/py.typed +0 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/settings.py +47 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/sync_client.py +144 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/version.py +1 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/PKG-INFO +260 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/SOURCES.txt +29 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/dependency_links.txt +1 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/entry_points.txt +2 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/requires.txt +19 -0
- tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/top_level.txt +1 -0
- tableau_hosted_mcp-0.1.0a1/tests/test_client_retry.py +98 -0
- tableau_hosted_mcp-0.1.0a1/tests/test_exceptions.py +20 -0
- tableau_hosted_mcp-0.1.0a1/tests/test_settings.py +59 -0
- tableau_hosted_mcp-0.1.0a1/tests/test_sync_client.py +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohamed Setit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tableau-hosted-mcp
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Python SDK for Tableau Hosted MCP
|
|
5
|
+
Author: Mohamed Setit
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: tableau,mcp,fastmcp,oauth,cimd,ai
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: fastmcp>=3.4.2
|
|
12
|
+
Requires-Dist: typer>=0.16
|
|
13
|
+
Requires-Dist: rich>=14.0
|
|
14
|
+
Requires-Dist: pydantic>=2.11
|
|
15
|
+
Requires-Dist: pydantic-settings>=2.5
|
|
16
|
+
Requires-Dist: platformdirs>=4.3
|
|
17
|
+
Requires-Dist: py-key-value-aio[disk]>=0.4
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy; extra == "dev"
|
|
23
|
+
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Provides-Extra: playground
|
|
25
|
+
Requires-Dist: streamlit>=1.29; extra == "playground"
|
|
26
|
+
Requires-Dist: anthropic>=0.40; extra == "playground"
|
|
27
|
+
Requires-Dist: openai>=1.40; extra == "playground"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# tableau-hosted-mcp
|
|
31
|
+
|
|
32
|
+
Python SDK for [Tableau Hosted MCP](https://mcp.tableau.com). Handles OAuth 2.1
|
|
33
|
+
PKCE via a CIMD-registered public client, persists tokens locally, and exposes
|
|
34
|
+
both an async and sync API plus a `tableau-mcp` CLI. Ready to drop into an LLM
|
|
35
|
+
agent — the MCP tool schemas map 1:1 to Anthropic and OpenAI tool-use formats.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install tableau-hosted-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11+.
|
|
44
|
+
|
|
45
|
+
## First run
|
|
46
|
+
|
|
47
|
+
The first call opens a browser tab to `sso.online.tableau.com`. Sign in once —
|
|
48
|
+
tokens are persisted to the platform's user-config directory (resolved via
|
|
49
|
+
[platformdirs](https://pypi.org/project/platformdirs/)) and reused on
|
|
50
|
+
subsequent runs.
|
|
51
|
+
|
|
52
|
+
| Platform | Default cache directory |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| macOS | `~/Library/Application Support/tableau-hosted-mcp/` |
|
|
55
|
+
| Linux | `~/.config/tableau-hosted-mcp/` (respects `$XDG_CONFIG_HOME`) |
|
|
56
|
+
| Windows | `%LOCALAPPDATA%\tableau-hosted-mcp\tableau-hosted-mcp\` |
|
|
57
|
+
|
|
58
|
+
Override with the `TABLEAU_MCP_CONFIG_DIR` env var if you want tokens on an
|
|
59
|
+
encrypted volume, a shared drive, or an ephemeral location. Storage is a
|
|
60
|
+
SQLite-backed key-value store (via `diskcache` under `py-key-value-aio`); no
|
|
61
|
+
native binaries, works identically on all three platforms.
|
|
62
|
+
|
|
63
|
+
The easiest way to warm up the token cache is:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
tableau-mcp login
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Library usage (async)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
from tableau_hosted_mcp import TableauHostedClient
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with TableauHostedClient() as client:
|
|
77
|
+
tools = await client.list_tools()
|
|
78
|
+
print(f"{len(tools)} tools available")
|
|
79
|
+
|
|
80
|
+
result = await client.call_tool("list-projects", {})
|
|
81
|
+
# result is fastmcp.client.client.CallToolResult:
|
|
82
|
+
# .is_error: bool
|
|
83
|
+
# .content: list[mcp.types.ContentBlock] (text/image/resource)
|
|
84
|
+
# .structured_content: dict | None
|
|
85
|
+
# .data: Any
|
|
86
|
+
print(result.content[0].text)
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Also available: `list_resources()`, `list_prompts()`, and `.fastmcp` for direct
|
|
92
|
+
access to the underlying `fastmcp.Client` if you need something the wrapper
|
|
93
|
+
doesn't expose.
|
|
94
|
+
|
|
95
|
+
## Library usage (sync)
|
|
96
|
+
|
|
97
|
+
For scripts, notebooks, Django management commands, or anything without an
|
|
98
|
+
event loop:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from tableau_hosted_mcp import TableauHostedClientSync
|
|
102
|
+
|
|
103
|
+
with TableauHostedClientSync() as client:
|
|
104
|
+
tools = client.list_tools()
|
|
105
|
+
result = client.call_tool("list-projects", {})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The sync facade runs a dedicated background thread with its own asyncio loop.
|
|
109
|
+
The connection persists across method calls — you pay OAuth cost once, not per
|
|
110
|
+
call. Do not share a single instance across threads.
|
|
111
|
+
|
|
112
|
+
## CLI
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
tableau-mcp login # run the OAuth flow, persist tokens
|
|
116
|
+
tableau-mcp logout # clear stored tokens (recovery for stale-cache errors)
|
|
117
|
+
tableau-mcp doctor # diagnose config + connectivity (CIMD reachable, MCP
|
|
118
|
+
# reachable, live list_tools round-trip)
|
|
119
|
+
tableau-mcp list-tools # list available MCP tools
|
|
120
|
+
tableau-mcp call-tool NAME [--arg key=value ...] [--json '{"k": "v"}']
|
|
121
|
+
# invoke a tool, print JSON result
|
|
122
|
+
tableau-mcp playground # open the Streamlit UI playground (requires [playground] extra)
|
|
123
|
+
tableau-mcp --version
|
|
124
|
+
tableau-mcp --log-level DEBUG # verbose httpx + MCP protocol logs
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Every subcommand honours the same env-var-driven config as the library API
|
|
128
|
+
(see below).
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
`Settings` reads from four sources, highest precedence first:
|
|
133
|
+
|
|
134
|
+
1. Constructor kwargs — `Settings(server_url=..., verify_ssl=False)`
|
|
135
|
+
2. Environment variables (prefix `TABLEAU_MCP_`):
|
|
136
|
+
- `TABLEAU_MCP_SERVER_URL` (default `https://mcp.tableau.com`)
|
|
137
|
+
- `TABLEAU_MCP_CIMD_URL` (default your CIMD `client.json` URL)
|
|
138
|
+
- `TABLEAU_MCP_CONFIG_DIR`
|
|
139
|
+
- `TABLEAU_MCP_LOG_LEVEL`
|
|
140
|
+
- `TABLEAU_MCP_VERIFY_SSL`
|
|
141
|
+
- `TABLEAU_MCP_TIMEOUT`
|
|
142
|
+
3. `.env` file in the current working directory
|
|
143
|
+
4. Built-in defaults
|
|
144
|
+
|
|
145
|
+
`Settings.from_file(path, **overrides)` also loads a JSON config file.
|
|
146
|
+
|
|
147
|
+
## Exceptions
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from tableau_hosted_mcp import (
|
|
151
|
+
TableauHostedMCPError, # base — catch this to handle any SDK failure
|
|
152
|
+
ConfigurationError, # invalid settings
|
|
153
|
+
AuthenticationError, # OAuth failed even after auto-retry with cleared tokens
|
|
154
|
+
ConnectionError, # transport failure reaching the MCP server (httpx-level)
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`connect()` automatically retries **once** on the stale-cache OAuth 500 pattern
|
|
159
|
+
by clearing the token store and re-authenticating. If that retry also fails,
|
|
160
|
+
you get an `AuthenticationError`. That means the manual `logout && login`
|
|
161
|
+
recovery step is normally invisible to callers.
|
|
162
|
+
|
|
163
|
+
## Using it inside an LLM agent
|
|
164
|
+
|
|
165
|
+
The MCP tool schemas exposed by `client.list_tools()` are plain JSON Schema
|
|
166
|
+
objects (`mcp.types.Tool.inputSchema`). They map 1:1 to the tool-use formats
|
|
167
|
+
used by:
|
|
168
|
+
|
|
169
|
+
- **Anthropic API** — `input_schema` field on tool specs
|
|
170
|
+
- **OpenAI API** — `parameters` field on function specs
|
|
171
|
+
- **Any framework built on those primitives** (LangChain, LlamaIndex, etc.)
|
|
172
|
+
|
|
173
|
+
The pattern for a manual agent loop is:
|
|
174
|
+
|
|
175
|
+
1. `tools = await client.list_tools()` — the 24 Tableau MCP tools
|
|
176
|
+
2. Convert each `Tool` to your LLM's tool spec (change the wrapper keys)
|
|
177
|
+
3. Pass the user's question + the tool specs to the LLM
|
|
178
|
+
4. LLM responds with tool_use blocks
|
|
179
|
+
5. For each tool_use, `await client.call_tool(name, arguments)`
|
|
180
|
+
6. Feed the tool results back as the next user message
|
|
181
|
+
7. Loop until the LLM returns a final text answer
|
|
182
|
+
|
|
183
|
+
See `examples/04_claude_agent.py` for a complete working implementation.
|
|
184
|
+
|
|
185
|
+
## Examples
|
|
186
|
+
|
|
187
|
+
The [`examples/`](./examples) directory contains runnable scripts:
|
|
188
|
+
|
|
189
|
+
| File | What it shows |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| [`01_quickstart_async.py`](examples/01_quickstart_async.py) | Connect, list tools, call `list-projects`. |
|
|
192
|
+
| [`02_quickstart_sync.py`](examples/02_quickstart_sync.py) | Same flow with the sync facade — no `asyncio` in your code. |
|
|
193
|
+
| [`03_data_pipeline.py`](examples/03_data_pipeline.py) | Chain `search-content` → `get-workbook` → `list-views` → `get-view-data`. Pure-SDK, no LLM. |
|
|
194
|
+
| [`04_claude_agent.py`](examples/04_claude_agent.py) | Wire the MCP tools into an Anthropic Claude agent loop. |
|
|
195
|
+
| [`05_openai_compat_agent.py`](examples/05_openai_compat_agent.py) | Same loop against any OpenAI-compatible endpoint (OpenAI, Groq, Together, Ollama, LM Studio, …). |
|
|
196
|
+
|
|
197
|
+
Run any of them from the repo root:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
python examples/01_quickstart_async.py
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The Claude example needs `pip install anthropic` and
|
|
204
|
+
`export ANTHROPIC_API_KEY=sk-ant-...`.
|
|
205
|
+
|
|
206
|
+
The OpenAI-compat example needs `pip install openai` and `OPENAI_API_KEY`.
|
|
207
|
+
Point at a non-OpenAI endpoint via `OPENAI_BASE_URL`, e.g.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
export OPENAI_BASE_URL=http://localhost:11434/v1 # Ollama
|
|
211
|
+
export OPENAI_MODEL=qwen2.5-coder:14b
|
|
212
|
+
export OPENAI_API_KEY=ollama # placeholder, endpoint ignores it
|
|
213
|
+
python examples/05_openai_compat_agent.py
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Playground
|
|
217
|
+
|
|
218
|
+
A Streamlit UI for exploring the SDK interactively — pick tools from a
|
|
219
|
+
dropdown, run them with an auto-generated form, chat with an LLM that has the
|
|
220
|
+
tools wired up, and inspect diagnostics + settings.
|
|
221
|
+
|
|
222
|
+
Install and launch:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install "tableau-hosted-mcp[playground]"
|
|
226
|
+
tableau-mcp playground
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Four tabs:
|
|
230
|
+
|
|
231
|
+
- **Tool Explorer** — every MCP tool's JSON schema becomes a form; run it and
|
|
232
|
+
see JSON / images / structured content rendered in place.
|
|
233
|
+
- **Agent Chat** — switch between Anthropic (Claude) and OpenAI-compatible
|
|
234
|
+
endpoints, watch each tool call unfold in a collapsible expander.
|
|
235
|
+
- **Diagnostics** — live version of `tableau-mcp doctor` plus token cache
|
|
236
|
+
contents.
|
|
237
|
+
- **Settings** — resolved effective values, which env vars are present, and
|
|
238
|
+
the `.env` file's contents.
|
|
239
|
+
|
|
240
|
+
Set `ANTHROPIC_API_KEY` and/or `OPENAI_API_KEY` (and `OPENAI_BASE_URL` for
|
|
241
|
+
non-OpenAI endpoints) before launching if you want the chat tab.
|
|
242
|
+
|
|
243
|
+
## Troubleshooting
|
|
244
|
+
|
|
245
|
+
**OAuth 500 from `sso.online.tableau.com`** — the SDK auto-retries after
|
|
246
|
+
clearing the token cache; you should rarely see this bubble up. If it persists,
|
|
247
|
+
run `tableau-mcp logout` and try again, or open your CIMD document
|
|
248
|
+
(`TABLEAU_MCP_CIMD_URL`) in a browser to confirm it's serving valid JSON.
|
|
249
|
+
|
|
250
|
+
**"Client failed to connect" with a stack trace** — run `tableau-mcp doctor`.
|
|
251
|
+
It probes CIMD reachability, MCP endpoint reachability, and full auth
|
|
252
|
+
round-trip, and points at whichever step failed.
|
|
253
|
+
|
|
254
|
+
**Tokens on the wrong tenant** — `tableau-mcp logout` clears the stored token
|
|
255
|
+
for the current `TABLEAU_MCP_SERVER_URL` value; run it before switching
|
|
256
|
+
tenants.
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# tableau-hosted-mcp
|
|
2
|
+
|
|
3
|
+
Python SDK for [Tableau Hosted MCP](https://mcp.tableau.com). Handles OAuth 2.1
|
|
4
|
+
PKCE via a CIMD-registered public client, persists tokens locally, and exposes
|
|
5
|
+
both an async and sync API plus a `tableau-mcp` CLI. Ready to drop into an LLM
|
|
6
|
+
agent — the MCP tool schemas map 1:1 to Anthropic and OpenAI tool-use formats.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install tableau-hosted-mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python 3.11+.
|
|
15
|
+
|
|
16
|
+
## First run
|
|
17
|
+
|
|
18
|
+
The first call opens a browser tab to `sso.online.tableau.com`. Sign in once —
|
|
19
|
+
tokens are persisted to the platform's user-config directory (resolved via
|
|
20
|
+
[platformdirs](https://pypi.org/project/platformdirs/)) and reused on
|
|
21
|
+
subsequent runs.
|
|
22
|
+
|
|
23
|
+
| Platform | Default cache directory |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| macOS | `~/Library/Application Support/tableau-hosted-mcp/` |
|
|
26
|
+
| Linux | `~/.config/tableau-hosted-mcp/` (respects `$XDG_CONFIG_HOME`) |
|
|
27
|
+
| Windows | `%LOCALAPPDATA%\tableau-hosted-mcp\tableau-hosted-mcp\` |
|
|
28
|
+
|
|
29
|
+
Override with the `TABLEAU_MCP_CONFIG_DIR` env var if you want tokens on an
|
|
30
|
+
encrypted volume, a shared drive, or an ephemeral location. Storage is a
|
|
31
|
+
SQLite-backed key-value store (via `diskcache` under `py-key-value-aio`); no
|
|
32
|
+
native binaries, works identically on all three platforms.
|
|
33
|
+
|
|
34
|
+
The easiest way to warm up the token cache is:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
tableau-mcp login
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Library usage (async)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from tableau_hosted_mcp import TableauHostedClient
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with TableauHostedClient() as client:
|
|
48
|
+
tools = await client.list_tools()
|
|
49
|
+
print(f"{len(tools)} tools available")
|
|
50
|
+
|
|
51
|
+
result = await client.call_tool("list-projects", {})
|
|
52
|
+
# result is fastmcp.client.client.CallToolResult:
|
|
53
|
+
# .is_error: bool
|
|
54
|
+
# .content: list[mcp.types.ContentBlock] (text/image/resource)
|
|
55
|
+
# .structured_content: dict | None
|
|
56
|
+
# .data: Any
|
|
57
|
+
print(result.content[0].text)
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Also available: `list_resources()`, `list_prompts()`, and `.fastmcp` for direct
|
|
63
|
+
access to the underlying `fastmcp.Client` if you need something the wrapper
|
|
64
|
+
doesn't expose.
|
|
65
|
+
|
|
66
|
+
## Library usage (sync)
|
|
67
|
+
|
|
68
|
+
For scripts, notebooks, Django management commands, or anything without an
|
|
69
|
+
event loop:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from tableau_hosted_mcp import TableauHostedClientSync
|
|
73
|
+
|
|
74
|
+
with TableauHostedClientSync() as client:
|
|
75
|
+
tools = client.list_tools()
|
|
76
|
+
result = client.call_tool("list-projects", {})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The sync facade runs a dedicated background thread with its own asyncio loop.
|
|
80
|
+
The connection persists across method calls — you pay OAuth cost once, not per
|
|
81
|
+
call. Do not share a single instance across threads.
|
|
82
|
+
|
|
83
|
+
## CLI
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
tableau-mcp login # run the OAuth flow, persist tokens
|
|
87
|
+
tableau-mcp logout # clear stored tokens (recovery for stale-cache errors)
|
|
88
|
+
tableau-mcp doctor # diagnose config + connectivity (CIMD reachable, MCP
|
|
89
|
+
# reachable, live list_tools round-trip)
|
|
90
|
+
tableau-mcp list-tools # list available MCP tools
|
|
91
|
+
tableau-mcp call-tool NAME [--arg key=value ...] [--json '{"k": "v"}']
|
|
92
|
+
# invoke a tool, print JSON result
|
|
93
|
+
tableau-mcp playground # open the Streamlit UI playground (requires [playground] extra)
|
|
94
|
+
tableau-mcp --version
|
|
95
|
+
tableau-mcp --log-level DEBUG # verbose httpx + MCP protocol logs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Every subcommand honours the same env-var-driven config as the library API
|
|
99
|
+
(see below).
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
`Settings` reads from four sources, highest precedence first:
|
|
104
|
+
|
|
105
|
+
1. Constructor kwargs — `Settings(server_url=..., verify_ssl=False)`
|
|
106
|
+
2. Environment variables (prefix `TABLEAU_MCP_`):
|
|
107
|
+
- `TABLEAU_MCP_SERVER_URL` (default `https://mcp.tableau.com`)
|
|
108
|
+
- `TABLEAU_MCP_CIMD_URL` (default your CIMD `client.json` URL)
|
|
109
|
+
- `TABLEAU_MCP_CONFIG_DIR`
|
|
110
|
+
- `TABLEAU_MCP_LOG_LEVEL`
|
|
111
|
+
- `TABLEAU_MCP_VERIFY_SSL`
|
|
112
|
+
- `TABLEAU_MCP_TIMEOUT`
|
|
113
|
+
3. `.env` file in the current working directory
|
|
114
|
+
4. Built-in defaults
|
|
115
|
+
|
|
116
|
+
`Settings.from_file(path, **overrides)` also loads a JSON config file.
|
|
117
|
+
|
|
118
|
+
## Exceptions
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from tableau_hosted_mcp import (
|
|
122
|
+
TableauHostedMCPError, # base — catch this to handle any SDK failure
|
|
123
|
+
ConfigurationError, # invalid settings
|
|
124
|
+
AuthenticationError, # OAuth failed even after auto-retry with cleared tokens
|
|
125
|
+
ConnectionError, # transport failure reaching the MCP server (httpx-level)
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`connect()` automatically retries **once** on the stale-cache OAuth 500 pattern
|
|
130
|
+
by clearing the token store and re-authenticating. If that retry also fails,
|
|
131
|
+
you get an `AuthenticationError`. That means the manual `logout && login`
|
|
132
|
+
recovery step is normally invisible to callers.
|
|
133
|
+
|
|
134
|
+
## Using it inside an LLM agent
|
|
135
|
+
|
|
136
|
+
The MCP tool schemas exposed by `client.list_tools()` are plain JSON Schema
|
|
137
|
+
objects (`mcp.types.Tool.inputSchema`). They map 1:1 to the tool-use formats
|
|
138
|
+
used by:
|
|
139
|
+
|
|
140
|
+
- **Anthropic API** — `input_schema` field on tool specs
|
|
141
|
+
- **OpenAI API** — `parameters` field on function specs
|
|
142
|
+
- **Any framework built on those primitives** (LangChain, LlamaIndex, etc.)
|
|
143
|
+
|
|
144
|
+
The pattern for a manual agent loop is:
|
|
145
|
+
|
|
146
|
+
1. `tools = await client.list_tools()` — the 24 Tableau MCP tools
|
|
147
|
+
2. Convert each `Tool` to your LLM's tool spec (change the wrapper keys)
|
|
148
|
+
3. Pass the user's question + the tool specs to the LLM
|
|
149
|
+
4. LLM responds with tool_use blocks
|
|
150
|
+
5. For each tool_use, `await client.call_tool(name, arguments)`
|
|
151
|
+
6. Feed the tool results back as the next user message
|
|
152
|
+
7. Loop until the LLM returns a final text answer
|
|
153
|
+
|
|
154
|
+
See `examples/04_claude_agent.py` for a complete working implementation.
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
The [`examples/`](./examples) directory contains runnable scripts:
|
|
159
|
+
|
|
160
|
+
| File | What it shows |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| [`01_quickstart_async.py`](examples/01_quickstart_async.py) | Connect, list tools, call `list-projects`. |
|
|
163
|
+
| [`02_quickstart_sync.py`](examples/02_quickstart_sync.py) | Same flow with the sync facade — no `asyncio` in your code. |
|
|
164
|
+
| [`03_data_pipeline.py`](examples/03_data_pipeline.py) | Chain `search-content` → `get-workbook` → `list-views` → `get-view-data`. Pure-SDK, no LLM. |
|
|
165
|
+
| [`04_claude_agent.py`](examples/04_claude_agent.py) | Wire the MCP tools into an Anthropic Claude agent loop. |
|
|
166
|
+
| [`05_openai_compat_agent.py`](examples/05_openai_compat_agent.py) | Same loop against any OpenAI-compatible endpoint (OpenAI, Groq, Together, Ollama, LM Studio, …). |
|
|
167
|
+
|
|
168
|
+
Run any of them from the repo root:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python examples/01_quickstart_async.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The Claude example needs `pip install anthropic` and
|
|
175
|
+
`export ANTHROPIC_API_KEY=sk-ant-...`.
|
|
176
|
+
|
|
177
|
+
The OpenAI-compat example needs `pip install openai` and `OPENAI_API_KEY`.
|
|
178
|
+
Point at a non-OpenAI endpoint via `OPENAI_BASE_URL`, e.g.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
export OPENAI_BASE_URL=http://localhost:11434/v1 # Ollama
|
|
182
|
+
export OPENAI_MODEL=qwen2.5-coder:14b
|
|
183
|
+
export OPENAI_API_KEY=ollama # placeholder, endpoint ignores it
|
|
184
|
+
python examples/05_openai_compat_agent.py
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Playground
|
|
188
|
+
|
|
189
|
+
A Streamlit UI for exploring the SDK interactively — pick tools from a
|
|
190
|
+
dropdown, run them with an auto-generated form, chat with an LLM that has the
|
|
191
|
+
tools wired up, and inspect diagnostics + settings.
|
|
192
|
+
|
|
193
|
+
Install and launch:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install "tableau-hosted-mcp[playground]"
|
|
197
|
+
tableau-mcp playground
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Four tabs:
|
|
201
|
+
|
|
202
|
+
- **Tool Explorer** — every MCP tool's JSON schema becomes a form; run it and
|
|
203
|
+
see JSON / images / structured content rendered in place.
|
|
204
|
+
- **Agent Chat** — switch between Anthropic (Claude) and OpenAI-compatible
|
|
205
|
+
endpoints, watch each tool call unfold in a collapsible expander.
|
|
206
|
+
- **Diagnostics** — live version of `tableau-mcp doctor` plus token cache
|
|
207
|
+
contents.
|
|
208
|
+
- **Settings** — resolved effective values, which env vars are present, and
|
|
209
|
+
the `.env` file's contents.
|
|
210
|
+
|
|
211
|
+
Set `ANTHROPIC_API_KEY` and/or `OPENAI_API_KEY` (and `OPENAI_BASE_URL` for
|
|
212
|
+
non-OpenAI endpoints) before launching if you want the chat tab.
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
**OAuth 500 from `sso.online.tableau.com`** — the SDK auto-retries after
|
|
217
|
+
clearing the token cache; you should rarely see this bubble up. If it persists,
|
|
218
|
+
run `tableau-mcp logout` and try again, or open your CIMD document
|
|
219
|
+
(`TABLEAU_MCP_CIMD_URL`) in a browser to confirm it's serving valid JSON.
|
|
220
|
+
|
|
221
|
+
**"Client failed to connect" with a stack trace** — run `tableau-mcp doctor`.
|
|
222
|
+
It probes CIMD reachability, MCP endpoint reachability, and full auth
|
|
223
|
+
round-trip, and points at whichever step failed.
|
|
224
|
+
|
|
225
|
+
**Tokens on the wrong tenant** — `tableau-mcp logout` clears the stored token
|
|
226
|
+
for the current `TABLEAU_MCP_SERVER_URL` value; run it before switching
|
|
227
|
+
tenants.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tableau-hosted-mcp"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Python SDK for Tableau Hosted MCP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Mohamed Setit" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
keywords = [
|
|
18
|
+
"tableau",
|
|
19
|
+
"mcp",
|
|
20
|
+
"fastmcp",
|
|
21
|
+
"oauth",
|
|
22
|
+
"cimd",
|
|
23
|
+
"ai"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"fastmcp>=3.4.2",
|
|
28
|
+
"typer>=0.16",
|
|
29
|
+
"rich>=14.0",
|
|
30
|
+
"pydantic>=2.11",
|
|
31
|
+
"pydantic-settings>=2.5",
|
|
32
|
+
"platformdirs>=4.3",
|
|
33
|
+
"py-key-value-aio[disk]>=0.4",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest",
|
|
39
|
+
"pytest-asyncio",
|
|
40
|
+
"ruff",
|
|
41
|
+
"mypy",
|
|
42
|
+
"build",
|
|
43
|
+
]
|
|
44
|
+
playground = [
|
|
45
|
+
"streamlit>=1.29",
|
|
46
|
+
"anthropic>=0.40",
|
|
47
|
+
"openai>=1.40",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
tableau-mcp = "tableau_hosted_mcp.cli:app"
|
|
52
|
+
|
|
53
|
+
[tool.setuptools]
|
|
54
|
+
package-dir = {"" = "src"}
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
asyncio_mode = "auto"
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.12"
|
|
68
|
+
strict = true
|
|
69
|
+
|
|
70
|
+
[[tool.mypy.overrides]]
|
|
71
|
+
# Streamlit ships no PEP 561 marker; keep the playground module wrappers
|
|
72
|
+
# permissive without weakening core SDK checks.
|
|
73
|
+
module = ["streamlit", "streamlit.*"]
|
|
74
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Tableau Hosted MCP SDK.
|
|
2
|
+
|
|
3
|
+
A Python SDK for the Tableau Hosted MCP server (https://mcp.tableau.com),
|
|
4
|
+
using OAuth 2.1 PKCE with a CIMD-registered public client.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import TableauHostedClient, connect
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
ConnectionError,
|
|
12
|
+
TableauHostedMCPError,
|
|
13
|
+
)
|
|
14
|
+
from .settings import Settings
|
|
15
|
+
from .sync_client import TableauHostedClientSync, connect_sync
|
|
16
|
+
from .version import __version__
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AuthenticationError",
|
|
20
|
+
"ConfigurationError",
|
|
21
|
+
"ConnectionError",
|
|
22
|
+
"Settings",
|
|
23
|
+
"TableauHostedClient",
|
|
24
|
+
"TableauHostedClientSync",
|
|
25
|
+
"TableauHostedMCPError",
|
|
26
|
+
"__version__",
|
|
27
|
+
"connect",
|
|
28
|
+
"connect_sync",
|
|
29
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..logging import configure_logging
|
|
6
|
+
from ..version import __version__
|
|
7
|
+
from . import doctor, login, logout, playground, tools
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="tableau-mcp",
|
|
11
|
+
help="Command-line interface for the Tableau Hosted MCP SDK.",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
login.register(app)
|
|
15
|
+
logout.register(app)
|
|
16
|
+
doctor.register(app)
|
|
17
|
+
tools.register(app)
|
|
18
|
+
playground.register(app)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback(invoke_without_command=True)
|
|
22
|
+
def _root(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
version: bool = typer.Option(
|
|
25
|
+
False,
|
|
26
|
+
"--version",
|
|
27
|
+
help="Print the SDK version and exit.",
|
|
28
|
+
is_eager=True,
|
|
29
|
+
),
|
|
30
|
+
log_level: str = typer.Option(
|
|
31
|
+
"INFO",
|
|
32
|
+
"--log-level",
|
|
33
|
+
help="Logging level (DEBUG, INFO, WARNING, ERROR).",
|
|
34
|
+
envvar="TABLEAU_MCP_LOG_LEVEL",
|
|
35
|
+
),
|
|
36
|
+
) -> None:
|
|
37
|
+
if version:
|
|
38
|
+
typer.echo(__version__)
|
|
39
|
+
raise typer.Exit()
|
|
40
|
+
configure_logging(log_level)
|
|
41
|
+
if ctx.invoked_subcommand is None:
|
|
42
|
+
typer.echo(ctx.get_help())
|
|
43
|
+
raise typer.Exit()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["app"]
|