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.
Files changed (31) hide show
  1. tableau_hosted_mcp-0.1.0a1/LICENSE +21 -0
  2. tableau_hosted_mcp-0.1.0a1/PKG-INFO +260 -0
  3. tableau_hosted_mcp-0.1.0a1/README.md +231 -0
  4. tableau_hosted_mcp-0.1.0a1/pyproject.toml +74 -0
  5. tableau_hosted_mcp-0.1.0a1/setup.cfg +4 -0
  6. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/__init__.py +29 -0
  7. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/__init__.py +46 -0
  8. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/doctor.py +75 -0
  9. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/login.py +26 -0
  10. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/logout.py +31 -0
  11. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/playground.py +76 -0
  12. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/cli/tools.py +77 -0
  13. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/client.py +150 -0
  14. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/constants.py +17 -0
  15. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/exceptions.py +14 -0
  16. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/logging.py +22 -0
  17. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/playground.py +626 -0
  18. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/py.typed +0 -0
  19. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/settings.py +47 -0
  20. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/sync_client.py +144 -0
  21. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp/version.py +1 -0
  22. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/PKG-INFO +260 -0
  23. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/SOURCES.txt +29 -0
  24. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/dependency_links.txt +1 -0
  25. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/entry_points.txt +2 -0
  26. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/requires.txt +19 -0
  27. tableau_hosted_mcp-0.1.0a1/src/tableau_hosted_mcp.egg-info/top_level.txt +1 -0
  28. tableau_hosted_mcp-0.1.0a1/tests/test_client_retry.py +98 -0
  29. tableau_hosted_mcp-0.1.0a1/tests/test_exceptions.py +20 -0
  30. tableau_hosted_mcp-0.1.0a1/tests/test_settings.py +59 -0
  31. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]