diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml
index f64365e5..77be7fea 100644
--- a/.github/workflows/test-deploy.yml
+++ b/.github/workflows/test-deploy.yml
@@ -29,3 +29,10 @@ jobs:
run: npm ci
- name: Test build website
run: npm run build
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts
+ if-no-files-found: error
+ path: |
+ docs/build/
diff --git a/docs/docs/configuration/mcp-config/authentication/README.md b/docs/docs/configuration/mcp-config/authentication/README.md
index c709cc2f..7741d1ca 100644
--- a/docs/docs/configuration/mcp-config/authentication/README.md
+++ b/docs/docs/configuration/mcp-config/authentication/README.md
@@ -9,3 +9,4 @@ There are a couple different ways to authenticate to Tableau.
1. Provide your Tableau [Personal Access Token](pat.md) (PAT).
2. Use Tableau [Connected Apps](direct-trust.md).
+3. Use Tableau OAuth.
diff --git a/docs/docs/configuration/mcp-config/authentication/_category_.json b/docs/docs/configuration/mcp-config/authentication/_category_.json
index 57cde766..01873b0a 100644
--- a/docs/docs/configuration/mcp-config/authentication/_category_.json
+++ b/docs/docs/configuration/mcp-config/authentication/_category_.json
@@ -1,4 +1,4 @@
{
"label": "Authentication",
- "position": 4
+ "position": 2
}
diff --git a/docs/docs/configuration/mcp-config/authentication/direct-trust.md b/docs/docs/configuration/mcp-config/authentication/direct-trust.md
index 8d48493f..d058ecb4 100644
--- a/docs/docs/configuration/mcp-config/authentication/direct-trust.md
+++ b/docs/docs/configuration/mcp-config/authentication/direct-trust.md
@@ -1,6 +1,5 @@
---
-sidebar_position: 3
-title: Direct Trust
+sidebar_position: 2
---
# Direct Trust
@@ -22,6 +21,8 @@ it internally calls into VizQL Data Service, the JWT will only have the
The username for the `sub` claim of the JWT.
+- Can either be a hard-coded username, or the OAuth username by setting it to `{OAUTH_USERNAME}`.
+
### `CONNECTED_APP_CLIENT_ID`
@@ -53,12 +54,13 @@ code where it could accidentally be revealed.
### `JWT_ADDITIONAL_PAYLOAD`
-A JSON string that includes any additional user attributes to include on the JWT.
+A JSON string that includes any additional user attributes to include on the JWT. It also supports
+dynamically including the OAuth username.
Example:
```json
-{ "region": "West" }
+{ "username": "{OAUTH_USERNAME}", "region": "West" }
```
[direct-trust]: https://help.tableau.com/current/online/en-us/connected_apps.htm#direct-trust
diff --git a/docs/docs/configuration/mcp-config/authentication/oauth.md b/docs/docs/configuration/mcp-config/authentication/oauth.md
new file mode 100644
index 00000000..93ddc349
--- /dev/null
+++ b/docs/docs/configuration/mcp-config/authentication/oauth.md
@@ -0,0 +1,22 @@
+---
+sidebar_position: 3
+---
+
+# OAuth
+
+:::warning
+
+Tableau Server 2025.3+ only. Tableau Cloud is not supported yet but is coming soon ETA Q2 2026.
+
+Otherwise, you can still test by running the MCP server locally.
+
+:::
+
+When `AUTH` is `oauth`, the MCP server will use a Tableau session initiated by the Tableau OAuth
+flow to authenticate to the Tableau REST APIs.
+
+:::info
+
+See [Enabling OAuth](../oauth.md) for details on how to configure the MCP server to use OAuth.
+
+:::
diff --git a/docs/docs/configuration/mcp-config/authentication/pat.md b/docs/docs/configuration/mcp-config/authentication/pat.md
index 30b173ec..3f28f1cf 100644
--- a/docs/docs/configuration/mcp-config/authentication/pat.md
+++ b/docs/docs/configuration/mcp-config/authentication/pat.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 2
+sidebar_position: 1
title: PAT
---
diff --git a/docs/docs/configuration/mcp-config/optional.md b/docs/docs/configuration/mcp-config/env-vars.md
similarity index 88%
rename from docs/docs/configuration/mcp-config/optional.md
rename to docs/docs/configuration/mcp-config/env-vars.md
index b06d449d..cc4724d9 100644
--- a/docs/docs/configuration/mcp-config/optional.md
+++ b/docs/docs/configuration/mcp-config/env-vars.md
@@ -1,10 +1,29 @@
---
-sidebar_position: 2
+sidebar_position: 1
---
-# Optional Environment Variables
+# Environment Variables
-Values for the following environment variables are optional.
+Values for the following environment variables can be provided to configure the Tableau MCP server.
+
+## `SERVER`
+
+The URL of the Tableau server.
+
+- For Tableau Cloud, specify your site's specific pod e.g.
+ `https://prod-useast-c.online.tableau.com`
+- Required unless [`AUTH`](#auth) is `oauth`.
+
+
+
+## `SITE_NAME`
+
+The name of the Tableau site to use.
+
+- For Tableau Cloud, specify your site name.
+- For Tableau Server, you may leave this value blank to use the default site.
+
+
## `TRANSPORT`
@@ -19,10 +38,10 @@ The MCP transport type to use for the server.
## `AUTH`
-The Tableau authentication method to use by the server.
+The method the MCP server uses to authenticate to the Tableau REST APIs.
- Default: `pat`
-- Possible values: `pat` or `direct-trust`
+- Possible values: `pat`, `direct-trust`, or `oauth`
- See [Authentication](authentication) for additional required variables depending on the desired
method.
@@ -43,6 +62,8 @@ APIs.
- Each line in the log file is a JSON object with the following properties:
- `timestamp`: The timestamp of the log message in UTC time.
+ - `username`: For tool calls, the username of the user who made the call. This is only present
+ when OAuth is enabled.
- `level`: The logging level of the log message.
- `logger`: The logger of the log message. This is typically `rest-api` for HTTP traces or
`tableau-mcp` for tool calls.
diff --git a/docs/docs/configuration/mcp-config/generator.mdx b/docs/docs/configuration/mcp-config/generator.mdx
new file mode 100644
index 00000000..0c67fdfe
--- /dev/null
+++ b/docs/docs/configuration/mcp-config/generator.mdx
@@ -0,0 +1,261 @@
+---
+sidebar_position: 5
+title: Config Generator
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import React, { useState } from 'react';
+
+export const EnvVarInput = ({ inputs, setInputs, reset }) => {
+ return (
+ <>
+
+
+
+ Clear values
+
+
+ {Object.entries(inputs).map(([k, v]) => (
+ <>
+
+ {v.docs ? {k} : k}
+
+
setInputs({ ...inputs, [k]: { ...v, value: e.target.value } })}
+ />
+ >
+ ))}
+
+
+ >
+ );
+};
+
+export const ConfigOutput = ({ type, inputs }) => {
+ let output = '';
+ let sanitizedOutput = '';
+
+ switch (type) {
+ case '.env':
+ output = Object.entries(inputs).map(([k, v]) => `${k}=${v.value ?? ''}`).join('\n');
+ sanitizedOutput = Object.entries(inputs).map(([k, v]) => `${k}=${v.isSensitive ? '*'.repeat(v.value?.length ?? 0) : v.value ?? ''}`).join('\n');
+ break;
+ case 'agent':
+ const env = Object.entries(inputs).reduce((acc, [k, v]) => ({ ...acc, [k]: v.value ?? '' }), {});
+ const sanitizedEnv = Object.entries(inputs).reduce((acc, [k, v]) => ({ ...acc, [k]: v.isSensitive ? '*'.repeat(v.value?.length ?? 0) : v.value ?? '' }), {});
+
+ const config = {
+ mcpServers: {
+ tableau: {
+ command: "node",
+ args: ["build/index.js"],
+ env
+ }
+ }
+ }
+ output = JSON.stringify(config, null, 2);
+ config.mcpServers.tableau.env = sanitizedEnv;
+ sanitizedOutput = JSON.stringify(config, null, 2);
+ break;
+ case 'vscode':
+ output = Object.entries(inputs).map(([k, v]) => `"tableau.mcp.${k}": "${v.value ?? ''}",`).join('\n');
+ sanitizedOutput = Object.entries(inputs).map(([k, v]) => `"tableau.mcp.${k}": "${v.isSensitive ? '*'.repeat(v.value?.length ?? 0) : v.value ?? ''}",`).join('\n');
+ break;
+ }
+
+ return (
+ <>
+
+
+
navigator.clipboard.writeText(output)}>Copy (secrets unmasked)
+
+ {sanitizedOutput}
+
+
+ >
+ );
+};
+
+export const ConfigGenerator = () => {
+ const defaultInputs = {
+ SERVER: {
+ docs: 'env-vars#server',
+ },
+ SITE_NAME: {
+ docs: 'env-vars#site-name',
+ },
+ AUTH: {
+ docs: 'env-vars#auth',
+ },
+ TRANSPORT: {
+ docs: 'env-vars#transport',
+ },
+ PAT_NAME: {
+ isSensitive: true,
+ docs: 'authentication/pat#pat_name',
+ },
+ PAT_VALUE: {
+ isSensitive: true,
+ docs: 'authentication/pat#pat_value',
+ },
+ DATASOURCE_CREDENTIALS: {
+ isSensitive: true,
+ docs: 'env-vars#datasource_credentials',
+ },
+ JWT_SUB_CLAIM: {
+ docs: 'authentication/direct-trust#jwt_sub_claim',
+ },
+ CONNECTED_APP_CLIENT_ID: {
+ docs: 'authentication/direct-trust#connected_app_client_id',
+ },
+ CONNECTED_APP_SECRET_ID: {
+ docs: 'authentication/direct-trust#connected_app_secret_id',
+ },
+ CONNECTED_APP_SECRET_VALUE: {
+ isSensitive: true,
+ docs: 'authentication/direct-trust#connected_app_secret_value',
+ },
+ JWT_ADDITIONAL_PAYLOAD: {
+ docs: 'authentication/direct-trust#jwt_additional_payload',
+ },
+ INCLUDE_TOOLS: {
+ docs: 'env-vars#include_tools',
+ },
+ EXCLUDE_TOOLS: {
+ docs: 'env-vars#exclude_tools',
+ },
+ MAX_RESULT_LIMIT: {
+ docs: 'env-vars#max_result_limit',
+ },
+ DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION: {
+ docs: 'env-vars#disable_query_datasource_filter_validation',
+ },
+ DISABLE_OAUTH: {
+ docs: 'oauth#disable_oauth',
+ },
+ OAUTH_ISSUER: {
+ docs: 'oauth#oauth_issuer',
+ },
+ OAUTH_JWE_PRIVATE_KEY_PATH: {
+ docs: 'oauth#oauth_jwe_private_key_path',
+ },
+ OAUTH_JWE_PRIVATE_KEY_PASSPHRASE: {
+ isSensitive: true,
+ docs: 'oauth#oauth_jwe_private_key_passphrase'
+ },
+ OAUTH_REDIRECT_URI: {
+ docs: 'oauth#oauth_redirect_uri'
+ },
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: {
+ docs: 'oauth#oauth_authorization_code_timeout_ms',
+ },
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: {
+ docs: 'oauth#oauth_access_token_timeout_ms',
+ },
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: {
+ docs: 'oauth#oauth_refresh_token_timeout_ms',
+ },
+ DEFAULT_LOG_LEVEL: {
+ docs: 'env-vars#default_log_level',
+ },
+ DISABLE_LOG_MASKING: {
+ docs: 'env-vars#disable_log_masking'
+ },
+ };
+
+ const [inputs, setInputs] = useState(defaultInputs);
+
+ const reset = () => {
+ setInputs(defaultInputs);
+ };
+
+ return (<>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >);
+}
+
+# Config Generator
+
+
\ No newline at end of file
diff --git a/docs/docs/configuration/mcp-config/http-server.md b/docs/docs/configuration/mcp-config/http-server.md
index 045d8af3..49b060f2 100644
--- a/docs/docs/configuration/mcp-config/http-server.md
+++ b/docs/docs/configuration/mcp-config/http-server.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 5
+sidebar_position: 3
---
# HTTP Server
@@ -7,6 +7,16 @@ sidebar_position: 5
The Tableau MCP server can be configured to run as an HTTP server, leveraging the Streaming HTTP MCP
transport. This is useful for deploying the server remotely and exposing it to multiple clients.
+:::warning
+
+When `TRANSPORT` is `http`, the default behavior changes to require protecting your MCP server with
+OAuth as a security best practice.
+
+To opt out of this behavior at your own risk, please see the entry on
+[`DISABLE_OAUTH`](oauth.md#disable_oauth).
+
+:::
+
When `TRANSPORT` is `http`, the following environment variables can be used to configure the HTTP
server. They are all optional.
diff --git a/docs/docs/configuration/mcp-config/oauth.md b/docs/docs/configuration/mcp-config/oauth.md
new file mode 100644
index 00000000..4d1ee1f5
--- /dev/null
+++ b/docs/docs/configuration/mcp-config/oauth.md
@@ -0,0 +1,309 @@
+---
+sidebar_position: 4
+---
+
+# Enabling OAuth
+
+:::warning
+
+Tableau Server 2025.3+ only. Tableau Cloud is not supported yet but is coming soon ETA Q2 2026.
+
+Otherwise, you can still test by running the MCP server locally.
+
+:::
+
+When a URL for `OAUTH_ISSUER` is provided, the MCP server will act as an OAuth 2.1 resource server,
+capable of accepting and responding to protected resource requests using encrypted access tokens.
+When enabled, MCP clients will first require logging in via Tableau OAuth to connect to the MCP
+server. For more information, please see the
+[MCP Authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
+
+
+
+## Environment Variables
+
+When OAuth is enabled, the following environment variables also apply or have additional meaning:
+
+### `AUTH`
+
+The method the MCP server uses to authenticate to the Tableau REST APIs.
+
+- Defaults to `oauth` when OAuth is enabled.
+- Can still be set to other authentication methods. See [Authentication](env-vars#auth) for options.
+- When set to a value _other_ than `oauth`, the MCP server will still be protected from unauthorized
+ access by OAuth but will _not_ use the Tableau session initiated by the Tableau OAuth flow to
+ authenticate to the Tableau REST APIs. For example, if `AUTH` is set to `pat`, the MCP server will
+ use the values of [`PAT_NAME`](authentication/pat#pat_name) and
+ [`PAT_VALUE`](authentication/pat#pat_value) to authenticate to the Tableau REST APIs.
+
+
+
+### `OAUTH_ISSUER`
+
+The issuer of the OAuth server. This should be the host of the MCP server.
+
+- Required if `AUTH` is `oauth`. For testing, use `http://127.0.0.1:3927`.
+- Required if `TRANSPORT` is `http` unless opted out with [`DISABLE_OAUTH`](#disable_oauth).
+
+
+
+### `TRANSPORT`
+
+The MCP transport type to use for the server.
+
+- Defaults to `http` when OAuth is enabled.
+- Must be `http` when OAuth is enabled.
+
+
+
+### `SERVER`
+
+- When [`AUTH`](#auth) is `oauth`, leave this empty to support any Tableau Cloud pod determined by
+ the site the user signed into when connecting to the MCP server.
+
+
+
+### `SITE_NAME`
+
+The target Tableau site for OAuth.
+
+- When [`AUTH`](#auth) is `oauth`, leaving this empty means any site will be supported, determined
+ by the site the user signed into when connecting to the MCP server.
+
+
+
+### `OAUTH_REDIRECT_URI`
+
+The redirect URI for the OAuth flow.
+
+- Default: `${OAUTH_ISSUER}/Callback`
+- Recommended to not define a value at all and just rely on its default value.
+- Path must be `/Callback` (case-sensitive).
+
+:::info
+
+Tableau Server administrators must also use
+[tsm](https://help.tableau.com/current/server/en-us/cli_configuration-set_tsm.htm) to set
+`oauth.allowed_redirect_uri_hosts` to the host of the MCP server.
+
+The value should be the same as [`OAUTH_ISSUER`](#oauth_issuer) but without the protocol or any
+trailing slash.
+
+```cmd
+tsm configuration set -k oauth.allowed_redirect_uri_hosts -v tableau-mcp.example.com
+tsm pending-changes apply
+```
+
+:::
+
+
+
+### `OAUTH_JWE_PRIVATE_KEY`
+
+The RSA private key used to decrypt the OAuth access token.
+
+- It or `OAUTH_JWE_PRIVATE_KEY_PATH` must be provided, but not both.
+- Only PEM format is supported.
+- Examples:
+
+ ```
+ -----BEGIN RSA PRIVATE KEY-----\nMIIE...HZ3Q==\n-----END RSA PRIVATE KEY-----
+
+ or
+
+ -----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIJ...te1w==\n-----END ENCRYPTED PRIVATE KEY-----
+ ```
+
+:::info
+
+The access token issued by the MCP server is encrypted using JWE (JSON Web Encryption) using an RSA
+public key. This public key is derived from the provided RSA private key.
+
+MCP clients provide this encrypted access token to the MCP server on the `Authorization` header of
+its requests. The MCP server decrypts the access token using the provided private key and uses the
+Tableau access token held within to authenticate subsequent requests to Tableau APIs. Any requests
+to the MCP server that do not have a valid access token will be rejected.
+
+If you need a private key, you can generate one using
+[openssl-genrsa](https://docs.openssl.org/3.0/man1/openssl-genrsa/) e.g.
+
+```cmd
+openssl genrsa -out private.pem
+```
+
+:::
+
+
+
+### `OAUTH_JWE_PRIVATE_KEY_PATH`
+
+The absolute path to the RSA private key (.pem) file used to decrypt the OAuth access token.
+
+- It or `OAUTH_JWE_PRIVATE_KEY` must be provided, but not both.
+- Only PEM format is supported.
+
+
+
+### `OAUTH_JWE_PRIVATE_KEY_PASSPHRASE`
+
+The passphrase for the private key if it is encrypted.
+
+
+
+### `OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS`
+
+The timeout for the OAuth authorization codes.
+
+- Default: 10 seconds.
+- Max: 1 hour.
+
+
+
+### `OAUTH_ACCESS_TOKEN_TIMEOUT_MS`
+
+The timeout for the OAuth access tokens.
+
+- Default: 1 hour.
+- Max: 30 days.
+
+
+
+### `OAUTH_REFRESH_TOKEN_TIMEOUT_MS`
+
+The timeout for the OAuth refresh tokens.
+
+- Default: 30 days.
+- Max: 1 year.
+
+
+
+### `DISABLE_OAUTH`
+
+:::warning
+
+When `TRANSPORT` is `http`, the default behavior changes to require protecting your MCP server with
+OAuth as a security best practice.
+
+To opt out of this behavior at your own risk, set `DISABLE_OAUTH` to `true`.
+
+This is not recommended as your MCP server will not be protected from unauthorized access. You will
+be prompted to accept these risks when you start the MCP server.
+
+:::
+
+- Default: `false`
+
+
+
+## Sequence Diagram
+
+```mermaid
+sequenceDiagram
+ participant Client as MCP Client (Claude/Cursor/VS Code)
+ participant MCP as MCP Server (OAuth Provider)
+ participant Tableau as Tableau Server (OAuth Authorization Server)
+ participant User as User (Browser)
+
+ Note over Client, User: MCP OAuth 2.1 Flow with PKCE
+
+ %% Step 1: Initial Request (401 Unauthorized)
+ Client->>MCP: 1. Request to protected resource (no Bearer token)
+ MCP->>Client: 2. 401 Unauthorized WWW-Authenticate: Bearer realm="MCP", resource_metadata="/.well-known/oauth-protected-resource"
+
+ %% Step 2: Resource Metadata Discovery
+ Client->>MCP: 3. GET /.well-known/oauth-protected-resource
+ MCP->>Client: 4. Resource metadata {resource, authorization_servers, bearer_methods_supported}
+
+ %% Step 3: Authorization Server Metadata
+ Client->>MCP: 5. GET /.well-known/oauth-authorization-server
+ MCP->>Client: 6. Authorization server metadata {issuer, authorization_endpoint, token_endpoint, registration_endpoint, response_types_supported, etc.}
+
+ %% Step 4: Dynamic Client Registration (Optional)
+ Client->>MCP: 7. POST /oauth/register {redirect_uris}
+ MCP->>Client: 8. Client registration response {client_id, redirect_uris, grant_types, response_types, token_endpoint_auth_method}
+
+ %% Step 5: Authorization Request with PKCE
+ Note over Client:Generate code_verifier and code_challenge (S256)
+ Client->>MCP: 9. GET /oauth/authorize? client_id=... &redirect_uri=... &response_type=code &code_challenge=... &code_challenge_method=S256 &state=...
+
+ Note over MCP: Generate tableauState, authKey Store pending authorization Generate tableauClientId
+ MCP->>Tableau: 10. Redirect to Tableau OAuth GET /oauth2/v1/auth? client_id=... &code_challenge=... &response_type=code &redirect_uri=/Callback &state=... &device_id=... &target_site=... &device_name=... &client_type=tableau-mcp
+
+ %% User Authorization
+ Tableau->>User: 11. OAuth login page
+ User->>Tableau: 12. Enter credentials & authorize
+ Tableau->>MCP: 13. Redirect to /Callback ?code=tableauAuthZCode &state=...
+
+ %% Step 6: OAuth Callback
+ Note over MCP: Validate state parameter Exchange Tableau authz code for tokens Get user session info
+ MCP->>Tableau: 14. POST /oauth2/v1/token {grant_type: "authorization_code", code: tableauAuthZCode, redirect_uri: /Callback, client_id: "...", code_verifier: "S256"}
+ Tableau->>MCP: 15. Token response {access_token, refresh_token, expires_in, origin_host}
+
+ Note over MCP: get server session to verify authentication
+ MCP->>Tableau: 16. GET /api/3.26/sessions/current
+
+ Note over MCP: Generate MCP authorization code Store authorization code with user info & tokens
+ MCP->>Client: 17. Redirect to client callback ?code=mcpAuthCode &state=originalState
+
+ %% Step 7: Token Exchange with PKCE Verification
+ Client->>MCP: 18. POST /oauth/token {grant_type: "authorization_code", code: mcpAuthCode, redirect_uri: ..., code_verifier: originalCodeVerifier, client_id: ...}
+
+ Note over MCP: Verify PKCE code_verifier Generate JWE access token Store refresh token
+ MCP->>Client: 19. Token response {access_token: JWE, token_type: "Bearer", expires_in: ..., refresh_token: ..., scope: "read"}
+
+ %% Step 8: Authenticated MCP Requests
+ Note over Client, MCP: Client can now make authenticated requests
+ Client->>MCP: 20. Request to protected resource Authorization: Bearer JWE_TOKEN
+ Note over MCP: Decrypt JWE, validate JWT Extract Tableau tokens & user info
+ MCP->>Tableau: 21. API call using Tableau access token
+ Tableau->>MCP: 22. API response
+ MCP->>Client: 23. MCP response with data
+
+ %% Token Refresh Flow (when needed)
+ Note over Client, MCP: When access token expires
+ Client->>MCP: 24. POST /oauth/token {grant_type: "refresh_token", refresh_token: refreshTokenId}
+ Note over MCP: Validate refresh token Try to refresh Tableau tokens Generate new JWE access token
+ MCP->>Tableau: 25. POST /oauth2/v1/token {grant_type: "refresh_token", refresh_token: ..., client_id: ...}
+ Tableau->>MCP: 26. New token response (or error)
+ MCP->>Client: 27. New access token or error
+
+ %% Error Handling
+ Note over Client, MCP: Error scenarios
+ alt Invalid/Expired Token
+ Client->>MCP: Request with invalid token
+ MCP->>Client: 401 Unauthorized {error: "invalid_token"}
+ Note over Client: Client should refresh token or re-authenticate
+ end
+
+ alt Tableau API Error
+ MCP->>Tableau: API call
+ Tableau->>MCP: Error response
+ MCP->>Client: Error response with details
+ end
+```
+
+### Key Components
+
+#### Security Features
+
+- **PKCE (RFC 7636)**: Code challenge/verifier for public clients
+- **JWE Encryption**: Access tokens encrypted with RSA-OAEP-256/A256GCM
+- **State Validation**: Prevents CSRF attacks
+- **Time-limited Tokens**: Authorization codes (10 seconds), access tokens (1 hour), refresh tokens
+ (30 days)
+
+#### Token Types
+
+1. **Tableau OAuth Tokens**: Direct from Tableau Server
+2. **MCP Authorization Code**: Short-lived code for token exchange
+3. **MCP Access Token**: JWE-encrypted token containing Tableau credentials
+4. **MCP Refresh Token**: For obtaining new access tokens
+
+#### Endpoints
+
+- `/.well-known/oauth-protected-resource`: Resource metadata discovery
+- `/.well-known/oauth-authorization-server`: Authorization server metadata
+- `/oauth/register`: Dynamic client registration
+- `/oauth/authorize`: Authorization endpoint with PKCE
+- `/Callback`: OAuth callback handler
+- `/oauth/token`: Token exchange and refresh
diff --git a/docs/docs/configuration/mcp-config/required.md b/docs/docs/configuration/mcp-config/required.md
deleted file mode 100644
index c75d8bf0..00000000
--- a/docs/docs/configuration/mcp-config/required.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-sidebar_position: 1
----
-
-# Required Environment Variables
-
-Values for the following environment variables must be provided to configure the Tableau MCP server.
-
-## `SERVER`
-
-The URL of the Tableau server.
-
-- For Tableau Cloud, specify your site's specific pod e.g.
- `https://prod-useast-c.online.tableau.com`
-
-
-
-## `SITE_NAME`
-
-The name of the Tableau site to use.
-
-- For Tableau Cloud, specify your site name.
-- For Tableau Server, you may leave this value blank to use the default site.
diff --git a/docs/docs/tools/data-qna/get-datasource-metadata.md b/docs/docs/tools/data-qna/get-datasource-metadata.md
index b1ad666a..1a736727 100644
--- a/docs/docs/tools/data-qna/get-datasource-metadata.md
+++ b/docs/docs/tools/data-qna/get-datasource-metadata.md
@@ -13,7 +13,7 @@ Fetches field metadata for the specified datasource.
## Environment variables
-- [`DISABLE_METADATA_API_REQUESTS`](../../configuration/mcp-config/optional.md#disable_metadata_api_requests)
+- [`DISABLE_METADATA_API_REQUESTS`](../../configuration/mcp-config/env-vars.md#disable_metadata_api_requests)
## Required arguments
diff --git a/docs/docs/tools/data-qna/list-datasources.md b/docs/docs/tools/data-qna/list-datasources.md
index 55ef9e52..fdc7fe3a 100644
--- a/docs/docs/tools/data-qna/list-datasources.md
+++ b/docs/docs/tools/data-qna/list-datasources.md
@@ -42,7 +42,7 @@ The maximum number of data sources to return. The tool will return at most this
Example: `2000`
-See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/optional.md#max_result_limit)
+See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/env-vars.md#max_result_limit)
## Example result
diff --git a/docs/docs/tools/data-qna/query-datasource.md b/docs/docs/tools/data-qna/query-datasource.md
index e8c5eb83..1f1dc1f7 100644
--- a/docs/docs/tools/data-qna/query-datasource.md
+++ b/docs/docs/tools/data-qna/query-datasource.md
@@ -13,7 +13,7 @@ data.
## Environment variables
-- [`DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION`](../../configuration/mcp-config/optional.md#disable_query_datasource_filter_validation)
+- [`DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION`](../../configuration/mcp-config/env-vars.md#disable_query_datasource_filter_validation)
## Required arguments
diff --git a/docs/docs/tools/views/list-views.md b/docs/docs/tools/views/list-views.md
index 2051661f..b2fc0bd7 100644
--- a/docs/docs/tools/views/list-views.md
+++ b/docs/docs/tools/views/list-views.md
@@ -42,7 +42,7 @@ The maximum number of views to return. The tool will return at most this many vi
Example: `2000`
-See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/optional.md#max_result_limit)
+See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/env-vars.md#max_result_limit)
## Example result
diff --git a/docs/docs/tools/workbooks/list-workbooks.md b/docs/docs/tools/workbooks/list-workbooks.md
index f206a2ff..426b4fe4 100644
--- a/docs/docs/tools/workbooks/list-workbooks.md
+++ b/docs/docs/tools/workbooks/list-workbooks.md
@@ -42,7 +42,7 @@ The maximum number of workbooks to return. The tool will return at most this man
Example: `2000`
-See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/optional.md#max_result_limit)
+See also: [`MAX_RESULT_LIMIT`](../../configuration/mcp-config/env-vars.md#max_result_limit)
## Example result
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index 6b13deb4..9c97813e 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -8,6 +8,10 @@ const config: Config = {
'Tableau MCP is a suite of developer primitives, including tools, resources and prompts, that will make it easier for developers to build AI applications that integrate with Tableau.',
favicon: 'img/favicon.ico',
trailingSlash: false,
+ markdown: {
+ mermaid: true,
+ },
+ themes: ['@docusaurus/theme-mermaid'],
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
future: {
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 0559785c..342d6938 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -11,6 +11,7 @@
"@docusaurus/core": "^3.9.0",
"@docusaurus/plugin-content-docs": "^3.9.0",
"@docusaurus/preset-classic": "^3.9.0",
+ "@docusaurus/theme-mermaid": "^3.8.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@@ -329,6 +330,28 @@
"node": ">= 14.0.0"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@antfu/utils": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz",
+ "integrity": "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -2029,6 +2052,51 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
+ "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
+ "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
+ "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
+ "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
+ "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
+ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -3852,6 +3920,28 @@
"react-dom": "^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@docusaurus/theme-mermaid": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.1.tgz",
+ "integrity": "sha512-IWYqjyTPjkNnHsFFu9+4YkeXS7PD1xI3Bn2shOhBq+f95mgDfWInkpfBN4aYvx4fTT67Am6cPtohRdwh4Tidtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@docusaurus/core": "3.8.1",
+ "@docusaurus/module-type-aliases": "3.8.1",
+ "@docusaurus/theme-common": "3.8.1",
+ "@docusaurus/types": "3.8.1",
+ "@docusaurus/utils-validation": "3.8.1",
+ "mermaid": ">=11.6.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.0.tgz",
@@ -4018,6 +4108,28 @@
"@hapi/hoek": "^9.0.0"
}
},
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.1.tgz",
+ "integrity": "sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@antfu/utils": "^9.2.0",
+ "@iconify/types": "^2.0.0",
+ "debug": "^4.4.1",
+ "globals": "^15.15.0",
+ "kolorist": "^1.8.0",
+ "local-pkg": "^1.1.1",
+ "mlly": "^1.7.4"
+ }
+ },
"node_modules/@jest/schemas": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
@@ -4275,6 +4387,15 @@
"react": ">=16"
}
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
+ "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "langium": "3.3.1"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4738,6 +4859,259 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4806,6 +5180,12 @@
"@types/send": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/gtag.js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
@@ -5042,6 +5422,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -6171,6 +6558,32 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/chevrotain": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
+ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "11.0.3",
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/regexp-to-ast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "@chevrotain/utils": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/chevrotain-allstar": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
+ "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ },
+ "peerDependencies": {
+ "chevrotain": "^11.0.0"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -6462,6 +6875,12 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "license": "MIT"
+ },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -6662,6 +7081,15 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -7090,61 +7518,587 @@
"postcss-unique-selectors": "^6.0.4"
},
"engines": {
- "node": "^14 || ^16 || >=18.0"
+ "node": "^14 || ^16 || >=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/cssnano-utils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz",
+ "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14 || ^16 || >=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/csso": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
+ "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "~2.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/css-tree": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
+ "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.28",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/mdn-data": {
+ "version": "2.0.28",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
+ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/cytoscape": {
+ "version": "3.33.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
+ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
},
- "peerDependencies": {
- "postcss": "^8.4.31"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/cssnano-utils": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz",
- "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==",
- "license": "MIT",
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
"engines": {
- "node": "^14 || ^16 || >=18.0"
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
},
- "peerDependencies": {
- "postcss": "^8.4.31"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/csso": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
- "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
- "license": "MIT",
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
"dependencies": {
- "css-tree": "~2.2.0"
+ "d3-array": "2 - 3"
},
"engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
+ "node": ">=12"
}
},
- "node_modules/csso/node_modules/css-tree": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
- "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
- "license": "MIT",
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
"dependencies": {
- "mdn-data": "2.0.28",
- "source-map-js": "^1.0.1"
+ "d3-time": "1 - 3"
},
"engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
+ "node": ">=12"
}
},
- "node_modules/csso/node_modules/mdn-data": {
- "version": "2.0.28",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
- "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
- "license": "CC0-1.0"
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz",
+ "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==",
+ "license": "MIT",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.18",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
+ "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"license": "MIT"
},
"node_modules/debounce": {
@@ -7308,6 +8262,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -7446,6 +8409,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -8049,6 +9021,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/exsolve": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+ "license": "MIT"
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -8560,6 +9538,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -8687,6 +9677,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -9374,6 +10370,15 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -9875,6 +10880,31 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/katex": {
+ "version": "0.16.22",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
+ "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -9884,6 +10914,11 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -9902,6 +10937,28 @@
"node": ">=6"
}
},
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+ "license": "MIT"
+ },
+ "node_modules/langium": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
+ "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
+ "license": "MIT",
+ "dependencies": {
+ "chevrotain": "~11.0.3",
+ "chevrotain-allstar": "~0.3.0",
+ "vscode-languageserver": "~9.0.1",
+ "vscode-languageserver-textdocument": "~1.0.11",
+ "vscode-uri": "~3.0.8"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/latest-version": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz",
@@ -9927,6 +10984,12 @@
"shell-quote": "^1.8.3"
}
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
+ "license": "MIT"
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9977,6 +11040,23 @@
"node": ">=8.9.0"
}
},
+ "node_modules/local-pkg": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+ "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^2.3.0",
+ "quansync": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
@@ -9998,6 +11078,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -10570,6 +11656,47 @@
"node": ">= 8"
}
},
+ "node_modules/mermaid": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.11.0.tgz",
+ "integrity": "sha512-9lb/VNkZqWTRjVgCV+l1N+t4kyi94y+l5xrmBmbbxZYkfRl5hEDaTPMOcaWKCl1McG8nBEaMlWwkcAEEgjhBgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.0.4",
+ "@iconify/utils": "^3.0.1",
+ "@mermaid-js/parser": "^0.6.2",
+ "@types/d3": "^7.4.3",
+ "cytoscape": "^3.29.3",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.11",
+ "dayjs": "^1.11.13",
+ "dompurify": "^3.2.5",
+ "katex": "^0.16.22",
+ "khroma": "^2.1.0",
+ "lodash-es": "^4.17.21",
+ "marked": "^15.0.7",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.6",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^11.1.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -12477,6 +13604,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mlly": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
+ "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.1"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "license": "MIT"
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -12951,6 +14107,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-manager-detector": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz",
+ "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==",
+ "license": "MIT"
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -13078,6 +14240,12 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
@@ -13126,6 +14294,12 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -13159,6 +14333,33 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -14766,6 +15967,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/quansync": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+ "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -15560,6 +16777,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+ "license": "Unlicense"
+ },
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
"node_modules/rtlcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
@@ -15613,6 +16848,12 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -16437,6 +17678,12 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -16660,6 +17907,12 @@
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
+ "node_modules/tinyexec": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+ "license": "MIT"
+ },
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
@@ -16735,6 +17988,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ts-dedent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -16810,6 +18072,12 @@
"node": ">=14.17"
}
},
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
+ "license": "MIT"
+ },
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
@@ -17313,6 +18581,55 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+ "license": "MIT"
+ },
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
diff --git a/docs/package.json b/docs/package.json
index b6fa6bd5..542a0e2c 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -18,6 +18,7 @@
"@docusaurus/core": "^3.9.0",
"@docusaurus/plugin-content-docs": "^3.9.0",
"@docusaurus/preset-classic": "^3.9.0",
+ "@docusaurus/theme-mermaid": "^3.8.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 27638ffd..ceceb63b 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -14,8 +14,9 @@ export default [
{
files: ['**/*.{js,mjs,cjs,ts}'],
rules: {
- 'no-console': 'error',
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
'no-duplicate-imports': ['error', { includeExports: true }],
+ quotes: ['error', 'single', { avoidEscape: true }],
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }],
diff --git a/src/config.test.ts b/src/config.test.ts
index 5c793865..0f6d7bee 100644
--- a/src/config.test.ts
+++ b/src/config.test.ts
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { exportedForTesting } from './config.js';
describe('Config', () => {
- const { Config } = exportedForTesting;
+ const { Config, parseNumber } = exportedForTesting;
const originalEnv = process.env;
@@ -41,6 +41,14 @@ describe('Config', () => {
MAX_RESULT_LIMIT: undefined,
DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION: undefined,
DISABLE_METADATA_API_REQUESTS: undefined,
+ DISABLE_OAUTH: undefined,
+ OAUTH_ISSUER: undefined,
+ OAUTH_REDIRECT_URI: undefined,
+ OAUTH_JWE_PRIVATE_KEY: undefined,
+ OAUTH_JWE_PRIVATE_KEY_PATH: undefined,
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: undefined,
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: undefined,
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: undefined,
};
});
@@ -52,6 +60,7 @@ describe('Config', () => {
process.env = {
...process.env,
SERVER: undefined,
+ SITE_NAME: 'test-site',
};
expect(() => new Config()).toThrow('The environment variable SERVER is not set');
@@ -61,6 +70,7 @@ describe('Config', () => {
process.env = {
...process.env,
SERVER: 'http://foo.com',
+ SITE_NAME: 'test-site',
};
expect(() => new Config()).toThrow(
@@ -72,6 +82,7 @@ describe('Config', () => {
process.env = {
...process.env,
SERVER: 'https://',
+ SITE_NAME: 'test-site',
};
expect(() => new Config()).toThrow(
@@ -270,6 +281,7 @@ describe('Config', () => {
...process.env,
...defaultEnvVars,
TRANSPORT: 'http',
+ DISABLE_OAUTH: 'true',
};
const config = new Config();
@@ -376,7 +388,49 @@ describe('Config', () => {
});
});
- describe('HTTP port parsing', () => {
+ describe('HTTP server config parsing', () => {
+ it('should set sslKey to default when SSL_KEY is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultEnvVars,
+ };
+
+ const config = new Config();
+ expect(config.sslKey).toBe('');
+ });
+
+ it('should set sslKey to the specified value when SSL_KEY is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultEnvVars,
+ SSL_KEY: 'path/to/ssl-key.pem',
+ };
+
+ const config = new Config();
+ expect(config.sslKey).toBe('path/to/ssl-key.pem');
+ });
+
+ it('should set sslCert to default when SSL_CERT is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultEnvVars,
+ };
+
+ const config = new Config();
+ expect(config.sslCert).toBe('');
+ });
+
+ it('should set sslCert to the specified value when SSL_CERT is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultEnvVars,
+ SSL_CERT: 'path/to/ssl-cert.pem',
+ };
+
+ const config = new Config();
+ expect(config.sslCert).toBe('path/to/ssl-cert.pem');
+ });
+
it('should set httpPort to default when HTTP_PORT_ENV_VAR_NAME and PORT are not set', () => {
process.env = {
...process.env,
@@ -647,4 +701,363 @@ describe('Config', () => {
expect(config.jwtAdditionalPayload).toBe('{}');
});
});
+
+ describe('OAuth configuration', () => {
+ const defaultOAuthEnvVars = {
+ ...defaultEnvVars,
+ OAUTH_ISSUER: 'https://example.com',
+ OAUTH_JWE_PRIVATE_KEY_PATH: 'path/to/private.pem',
+ } as const;
+
+ const defaultOAuthTimeoutMs = {
+ authzCodeTimeoutMs: 10 * 60 * 1000,
+ accessTokenTimeoutMs: 1 * 60 * 60 * 1000,
+ refreshTokenTimeoutMs: 30 * 24 * 60 * 60 * 1000,
+ };
+
+ const defaultOAuthConfig = {
+ enabled: true,
+ issuer: defaultOAuthEnvVars.OAUTH_ISSUER,
+ redirectUri: `${defaultOAuthEnvVars.OAUTH_ISSUER}/Callback`,
+ jwePrivateKey: '',
+ jwePrivateKeyPath: defaultOAuthEnvVars.OAUTH_JWE_PRIVATE_KEY_PATH,
+ ...defaultOAuthTimeoutMs,
+ } as const;
+
+ it('should default to disabled', () => {
+ process.env = {
+ ...process.env,
+ ...defaultEnvVars,
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ enabled: false,
+ issuer: '',
+ redirectUri: '',
+ jwePrivateKey: '',
+ jwePrivateKeyPath: '',
+ ...defaultOAuthTimeoutMs,
+ });
+ });
+
+ it('should enable OAuth when OAUTH_ISSUER is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual(defaultOAuthConfig);
+ });
+
+ it('should disable OAuth when DISABLE_OAUTH is "true"', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ DISABLE_OAUTH: 'true',
+ };
+
+ const config = new Config();
+ expect(config.oauth.enabled).toEqual(false);
+ });
+
+ it('should set redirectUri to the specified value when OAUTH_REDIRECT_URI is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_REDIRECT_URI: 'https://example.com/CustomCallback',
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ ...defaultOAuthConfig,
+ redirectUri: 'https://example.com/CustomCallback',
+ });
+ });
+
+ it('should set redirectUri to the default value when OAUTH_REDIRECT_URI is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_REDIRECT_URI: '',
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ ...defaultOAuthConfig,
+ redirectUri: `${defaultOAuthEnvVars.OAUTH_ISSUER}/Callback`,
+ });
+ });
+
+ it('should set jwePrivateKey to the specified value when OAUTH_JWE_PRIVATE_KEY is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_JWE_PRIVATE_KEY: 'hamburgers',
+ OAUTH_JWE_PRIVATE_KEY_PATH: '',
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ ...defaultOAuthConfig,
+ jwePrivateKey: 'hamburgers',
+ jwePrivateKeyPath: '',
+ jwePrivateKeyPassphrase: undefined,
+ });
+ });
+
+ it('should set authzCodeTimeoutMs to the specified value when OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: '5678',
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ ...defaultOAuthConfig,
+ authzCodeTimeoutMs: 5678,
+ });
+ });
+
+ it('should set accessTokenTimeoutMs to the specified value when OAUTH_ACCESS_TOKEN_TIMEOUT_MS is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: '1234',
+ };
+
+ const config = new Config();
+ expect(config.oauth).toEqual({
+ ...defaultOAuthConfig,
+ accessTokenTimeoutMs: 1234,
+ });
+ });
+
+ it('should set refreshTokenTimeoutMs to the specified value when OAUTH_REFRESH_TOKEN_TIMEOUT_MS is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: '1234',
+ };
+
+ const config = new Config();
+ expect(config.oauth.refreshTokenTimeoutMs).toBe(1234);
+ });
+
+ it('should throw error when TRANSPORT is "http" and OAUTH_ISSUER is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ TRANSPORT: 'http',
+ OAUTH_ISSUER: undefined,
+ };
+
+ expect(() => new Config()).toThrow(
+ 'OAUTH_ISSUER must be set when TRANSPORT is "http" unless DISABLE_OAUTH is "true"',
+ );
+ });
+
+ it('should throw error when OAUTH_JWE_PRIVATE_KEY and OAUTH_JWE_PRIVATE_KEY_PATH is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_JWE_PRIVATE_KEY_PATH: '',
+ };
+
+ expect(() => new Config()).toThrow(
+ 'One of the environment variables: OAUTH_JWE_PRIVATE_KEY_PATH or OAUTH_JWE_PRIVATE_KEY must be set',
+ );
+ });
+
+ it('should throw error when OAUTH_JWE_PRIVATE_KEY and OAUTH_JWE_PRIVATE_KEY_PATH are both set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ OAUTH_JWE_PRIVATE_KEY: 'hamburgers',
+ OAUTH_JWE_PRIVATE_KEY_PATH: 'hotdogs',
+ };
+
+ expect(() => new Config()).toThrow(
+ 'Only one of the environment variables: OAUTH_JWE_PRIVATE_KEY or OAUTH_JWE_PRIVATE_KEY_PATH must be set',
+ );
+ });
+
+ it('should throw error when AUTH is "oauth" and OAUTH_ISSUER is not set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ AUTH: 'oauth',
+ OAUTH_ISSUER: '',
+ };
+
+ expect(() => new Config()).toThrow('When AUTH is "oauth", OAUTH_ISSUER must be set');
+ });
+
+ it('should throw error when AUTH is "oauth" and DISABLE_OAUTH is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ AUTH: 'oauth',
+ DISABLE_OAUTH: 'true',
+ };
+
+ expect(() => new Config()).toThrow('When AUTH is "oauth", DISABLE_OAUTH cannot be "true"');
+ });
+
+ it('should default transport to "http" when OAUTH_ISSUER is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ TRANSPORT: undefined,
+ };
+
+ const config = new Config();
+ expect(config.transport).toBe('http');
+ });
+
+ it('should default auth to "oauth" when OAUTH_ISSUER is set', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ };
+
+ const config = new Config();
+ expect(config.auth).toBe('oauth');
+ });
+
+ it('should throw error when transport is stdio and auth is "oauth"', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ TRANSPORT: 'stdio',
+ };
+
+ expect(() => new Config()).toThrow('TRANSPORT must be "http" when OAUTH_ISSUER is set');
+ });
+
+ it('should allow PAT_NAME and PAT_VALUE to be empty when AUTH is "oauth"', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ PAT_NAME: undefined,
+ PAT_VALUE: undefined,
+ AUTH: 'oauth',
+ };
+
+ const config = new Config();
+ expect(config.patName).toBe('');
+ expect(config.patValue).toBe('');
+ });
+
+ it('should allow SITE_NAME to be empty when AUTH is "oauth"', () => {
+ process.env = {
+ ...process.env,
+ ...defaultOAuthEnvVars,
+ AUTH: 'oauth',
+ SITE_NAME: '',
+ };
+
+ const config = new Config();
+ expect(config.siteName).toBe('');
+ });
+ });
+
+ describe('parseNumber', () => {
+ it('should return defaultValue when value is undefined', () => {
+ const result = parseNumber(undefined, { defaultValue: 42 });
+ expect(result).toBe(42);
+ });
+
+ it('should return defaultValue when value is empty string', () => {
+ const result = parseNumber('', { defaultValue: 42 });
+ expect(result).toBe(42);
+ });
+
+ it('should return defaultValue when value is whitespace', () => {
+ const result = parseNumber(' ', { defaultValue: 42 });
+ expect(result).toBe(42);
+ });
+
+ it('should return defaultValue when value is not a number', () => {
+ const result = parseNumber('abc', { defaultValue: 42 });
+ expect(result).toBe(42);
+ });
+
+ it('should return defaultValue when value is NaN', () => {
+ const result = parseNumber('NaN', { defaultValue: 42 });
+ expect(result).toBe(42);
+ });
+
+ it('should parse valid integer string', () => {
+ const result = parseNumber('123', { defaultValue: 42 });
+ expect(result).toBe(123);
+ });
+
+ it('should parse valid integer string with leading zeros', () => {
+ const result = parseNumber('007', { defaultValue: 42 });
+ expect(result).toBe(7);
+ });
+
+ it('should parse valid integer string with whitespace', () => {
+ const result = parseNumber(' 456 ', { defaultValue: 42 });
+ expect(result).toBe(456);
+ });
+
+ it('should parse valid decimal string', () => {
+ const result = parseNumber('123.45', { defaultValue: 42 });
+ expect(result).toBe(123.45);
+ });
+
+ it('should parse valid decimal string with whitespace', () => {
+ const result = parseNumber(' 123.45 ', { defaultValue: 42 });
+ expect(result).toBe(123.45);
+ });
+
+ it('should return defaultValue when value is below minValue', () => {
+ const result = parseNumber('5', { defaultValue: 42, minValue: 10 });
+ expect(result).toBe(42);
+ });
+
+ it('should return defaultValue when value is above maxValue', () => {
+ const result = parseNumber('100', { defaultValue: 42, maxValue: 50 });
+ expect(result).toBe(42);
+ });
+
+ it('should parse valid number when within minValue and maxValue range', () => {
+ const result = parseNumber('25', { defaultValue: 42, minValue: 10, maxValue: 50 });
+ expect(result).toBe(25);
+ });
+
+ it('should parse valid number when value equals minValue', () => {
+ const result = parseNumber('10', { defaultValue: 42, minValue: 10, maxValue: 50 });
+ expect(result).toBe(10);
+ });
+
+ it('should parse valid number when value equals maxValue', () => {
+ const result = parseNumber('50', { defaultValue: 42, minValue: 10, maxValue: 50 });
+ expect(result).toBe(50);
+ });
+
+ it('should use default options when no options provided', () => {
+ const result = parseNumber('123');
+ expect(result).toBe(123);
+ });
+
+ it('should use default defaultValue of 0 when no options provided', () => {
+ const result = parseNumber('abc');
+ expect(result).toBe(0);
+ });
+
+ it('should handle negative numbers with appropriate minValue', () => {
+ const result = parseNumber('-5', { defaultValue: 42, minValue: -10 });
+ expect(result).toBe(-5);
+ });
+
+ it('should return defaultValue for negative numbers when minValue is 0', () => {
+ const result = parseNumber('-5', { defaultValue: 42, minValue: 0 });
+ expect(result).toBe(42);
+ });
+ });
});
diff --git a/src/config.ts b/src/config.ts
index 7cb11d42..ccb88809 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,4 +1,5 @@
import { CorsOptions } from 'cors';
+import { existsSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
@@ -8,9 +9,18 @@ import invariant from './utils/invariant.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
-const authTypes = ['pat', 'direct-trust'] as const;
+const TEN_MINUTES_IN_MS = 10 * 60 * 1000;
+const ONE_HOUR_IN_MS = 60 * 60 * 1000;
+const THIRTY_DAYS_IN_MS = 30 * 24 * 60 * 60 * 1000;
+const ONE_YEAR_IN_MS = 365.25 * 24 * 60 * 60 * 1000;
+
+const authTypes = ['pat', 'direct-trust', 'oauth'] as const;
type AuthType = (typeof authTypes)[number];
+function isAuthType(auth: unknown): auth is AuthType {
+ return !!authTypes.find((type) => type === auth);
+}
+
export class Config {
auth: AuthType;
server: string;
@@ -37,6 +47,17 @@ export class Config {
disableMetadataApiRequests: boolean;
enableServerLogging: boolean;
serverLogDirectory: string;
+ oauth: {
+ enabled: boolean;
+ issuer: string;
+ redirectUri: string;
+ jwePrivateKey: string;
+ jwePrivateKeyPath: string;
+ jwePrivateKeyPassphrase: string | undefined;
+ authzCodeTimeoutMs: number;
+ accessTokenTimeoutMs: number;
+ refreshTokenTimeoutMs: number;
+ };
constructor() {
const cleansedVars = removeClaudeMcpBundleUserConfigTemplates(process.env);
@@ -59,6 +80,15 @@ export class Config {
DATASOURCE_CREDENTIALS: datasourceCredentials,
DEFAULT_LOG_LEVEL: defaultLogLevel,
DISABLE_LOG_MASKING: disableLogMasking,
+ DISABLE_OAUTH: disableOauth,
+ OAUTH_ISSUER: oauthIssuer,
+ OAUTH_JWE_PRIVATE_KEY: oauthJwePrivateKey,
+ OAUTH_JWE_PRIVATE_KEY_PATH: oauthJwePrivateKeyPath,
+ OAUTH_JWE_PRIVATE_KEY_PASSPHRASE: oauthJwePrivateKeyPassphrase,
+ OAUTH_REDIRECT_URI: redirectUri,
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: authzCodeTimeoutMs,
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: accessTokenTimeoutMs,
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: refreshTokenTimeoutMs,
INCLUDE_TOOLS: includeTools,
EXCLUDE_TOOLS: excludeTools,
MAX_RESULT_LIMIT: maxResultLimit,
@@ -68,16 +98,15 @@ export class Config {
SERVER_LOG_DIRECTORY: serverLogDirectory,
} = cleansedVars;
- const defaultPort = 3927;
- const httpPort = cleansedVars[httpPortEnvVarName?.trim() || 'PORT'] || defaultPort.toString();
- const httpPortNumber = parseInt(httpPort, 10);
-
this.siteName = siteName ?? '';
- this.auth = authTypes.find((type) => type === auth) ?? 'pat';
- this.transport = isTransport(transport) ? transport : 'stdio';
+
this.sslKey = sslKey?.trim() ?? '';
this.sslCert = sslCert?.trim() ?? '';
- this.httpPort = isNaN(httpPortNumber) ? defaultPort : httpPortNumber;
+ this.httpPort = parseNumber(cleansedVars[httpPortEnvVarName?.trim() || 'PORT'], {
+ defaultValue: 3927,
+ minValue: 1,
+ maxValue: 65535,
+ });
this.corsOriginConfig = getCorsOriginConfig(corsOriginConfig?.trim() ?? '');
this.datasourceCredentials = datasourceCredentials ?? '';
this.defaultLogLevel = defaultLogLevel ?? 'debug';
@@ -87,6 +116,83 @@ export class Config {
this.enableServerLogging = enableServerLogging === 'true';
this.serverLogDirectory = serverLogDirectory || join(__dirname, 'logs');
+ const disableOauthOverride = disableOauth === 'true';
+ this.oauth = {
+ enabled: disableOauthOverride ? false : !!oauthIssuer,
+ issuer: oauthIssuer ?? '',
+ redirectUri: redirectUri || (oauthIssuer ? `${oauthIssuer}/Callback` : ''),
+ jwePrivateKey: oauthJwePrivateKey ?? '',
+ jwePrivateKeyPath: oauthJwePrivateKeyPath ?? '',
+ jwePrivateKeyPassphrase: oauthJwePrivateKeyPassphrase || undefined,
+ authzCodeTimeoutMs: parseNumber(authzCodeTimeoutMs, {
+ defaultValue: TEN_MINUTES_IN_MS,
+ minValue: 0,
+ maxValue: ONE_HOUR_IN_MS,
+ }),
+ accessTokenTimeoutMs: parseNumber(accessTokenTimeoutMs, {
+ defaultValue: ONE_HOUR_IN_MS,
+ minValue: 0,
+ maxValue: THIRTY_DAYS_IN_MS,
+ }),
+ refreshTokenTimeoutMs: parseNumber(refreshTokenTimeoutMs, {
+ defaultValue: THIRTY_DAYS_IN_MS,
+ minValue: 0,
+ maxValue: ONE_YEAR_IN_MS,
+ }),
+ };
+
+ this.auth = isAuthType(auth) ? auth : this.oauth.enabled ? 'oauth' : 'pat';
+ this.transport = isTransport(transport) ? transport : this.oauth.enabled ? 'http' : 'stdio';
+
+ if (this.transport === 'http' && !disableOauthOverride && !this.oauth.issuer) {
+ throw new Error(
+ 'OAUTH_ISSUER must be set when TRANSPORT is "http" unless DISABLE_OAUTH is "true"',
+ );
+ }
+
+ if (this.auth === 'oauth') {
+ if (disableOauthOverride) {
+ throw new Error('When AUTH is "oauth", DISABLE_OAUTH cannot be "true"');
+ }
+
+ if (!this.oauth.issuer) {
+ throw new Error('When AUTH is "oauth", OAUTH_ISSUER must be set');
+ }
+ } else {
+ invariant(server, 'The environment variable SERVER is not set');
+ validateServer(server);
+ }
+
+ if (this.oauth.enabled) {
+ invariant(this.oauth.redirectUri, 'The environment variable OAUTH_REDIRECT_URI is not set');
+
+ if (!this.oauth.jwePrivateKey && !this.oauth.jwePrivateKeyPath) {
+ throw new Error(
+ 'One of the environment variables: OAUTH_JWE_PRIVATE_KEY_PATH or OAUTH_JWE_PRIVATE_KEY must be set',
+ );
+ }
+
+ if (this.oauth.jwePrivateKey && this.oauth.jwePrivateKeyPath) {
+ throw new Error(
+ 'Only one of the environment variables: OAUTH_JWE_PRIVATE_KEY or OAUTH_JWE_PRIVATE_KEY_PATH must be set',
+ );
+ }
+
+ if (
+ this.oauth.jwePrivateKeyPath &&
+ process.env.TABLEAU_MCP_TEST !== 'true' &&
+ !existsSync(this.oauth.jwePrivateKeyPath)
+ ) {
+ throw new Error(
+ `OAuth JWE private key path does not exist: ${this.oauth.jwePrivateKeyPath}`,
+ );
+ }
+
+ if (this.transport === 'stdio') {
+ throw new Error('TRANSPORT must be "http" when OAUTH_ISSUER is set');
+ }
+ }
+
const maxResultLimitNumber = maxResultLimit ? parseInt(maxResultLimit) : NaN;
this.maxResultLimit =
isNaN(maxResultLimitNumber) || maxResultLimitNumber <= 0 ? null : maxResultLimitNumber;
@@ -109,9 +215,6 @@ export class Config {
throw new Error('Cannot include and exclude tools simultaneously');
}
- invariant(server, 'The environment variable SERVER is not set');
- validateServer(server);
-
if (this.auth === 'pat') {
invariant(patName, 'The environment variable PAT_NAME is not set');
invariant(patValue, 'The environment variable PAT_VALUE is not set');
@@ -122,7 +225,7 @@ export class Config {
invariant(secretValue, 'The environment variable CONNECTED_APP_SECRET_VALUE is not set');
}
- this.server = server;
+ this.server = server ?? '';
this.patName = patName ?? '';
this.patValue = patValue ?? '';
this.jwtSubClaim = jwtSubClaim ?? '';
@@ -196,8 +299,33 @@ function removeClaudeMcpBundleUserConfigTemplates(
}, {});
}
+function parseNumber(
+ value: string | undefined,
+ {
+ defaultValue,
+ minValue,
+ maxValue,
+ }: { defaultValue: number; minValue?: number; maxValue?: number } = {
+ defaultValue: 0,
+ minValue: Number.NEGATIVE_INFINITY,
+ maxValue: Number.POSITIVE_INFINITY,
+ },
+): number {
+ if (!value) {
+ return defaultValue;
+ }
+
+ const number = parseFloat(value);
+ return isNaN(number) ||
+ (minValue !== undefined && number < minValue) ||
+ (maxValue !== undefined && number > maxValue)
+ ? defaultValue
+ : number;
+}
+
export const getConfig = (): Config => new Config();
export const exportedForTesting = {
Config,
+ parseNumber,
};
diff --git a/src/index.ts b/src/index.ts
index 57b95bbc..036d916e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import dotenv from 'dotenv';
+import readline from 'readline/promises';
import { getConfig } from './config.js';
import { isLoggingLevel, log, setLogLevel, setServerLogger, writeToStderr } from './logging/log.js';
@@ -34,6 +35,30 @@ async function startServer(): Promise {
case 'http': {
const { url } = await startExpressServer({ basePath: serverName, config, logLevel });
+ if (!config.oauth.enabled) {
+ console.warn(
+ '⚠️ TRANSPORT is "http" but OAuth is disabled! Your MCP server may not be protected from unauthorized access!',
+ );
+
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+
+ const input = (
+ await rl.question('⚠️ To accept these risks, please type "I accept" and press Enter.\n> ')
+ )
+ .toLocaleLowerCase()
+ .replaceAll('"', '')
+ .trim();
+
+ if (input !== 'i accept') {
+ // eslint-disable-next-line no-console -- console.log is intentional here since the transport is not stdio.
+ console.log('Goodbye 👋');
+ process.exit(1);
+ }
+ }
+
// eslint-disable-next-line no-console -- console.log is intentional here since the transport is not stdio.
console.log(
`${serverName} v${serverVersion} stateless streamable HTTP server available at ${url}`,
@@ -43,7 +68,7 @@ async function startServer(): Promise {
}
if (config.disableLogMasking) {
- writeToStderr('Log masking is disabled!');
+ writeToStderr('⚠️ Log masking is disabled!');
}
}
diff --git a/src/logging/log.ts b/src/logging/log.ts
index 7b7a9f51..6a5485ce 100644
--- a/src/logging/log.ts
+++ b/src/logging/log.ts
@@ -85,14 +85,17 @@ export const getToolLogMessage = ({
requestId,
toolName,
args,
+ username,
}: {
requestId: RequestId;
toolName: ToolName;
args: unknown;
+ username?: string;
}): LogMessage => {
return {
type: 'tool',
requestId,
+ ...(username ? { username } : {}),
tool: {
name: toolName,
...(args !== undefined ? { args } : {}),
diff --git a/src/restApiInstance.test.ts b/src/restApiInstance.test.ts
index 596ef6e7..023cef93 100644
--- a/src/restApiInstance.test.ts
+++ b/src/restApiInstance.test.ts
@@ -12,6 +12,7 @@ import {
import { AuthConfig } from './sdks/tableau/authConfig.js';
import RestApi from './sdks/tableau/restApi.js';
import { Server } from './server.js';
+import { userAgent } from './server/userAgent.js';
vi.mock('./sdks/tableau/restApi.js', () => ({
default: vi.fn().mockImplementation(() => ({
@@ -71,7 +72,7 @@ describe('restApiInstance', () => {
interceptor(mockRequest);
- expect(mockRequest.headers['User-Agent']).toBe(`${server.name}/${server.version}`);
+ expect(mockRequest.headers['User-Agent']).toBe(userAgent);
expect(log.info).toHaveBeenCalledWith(
server,
expect.objectContaining({
diff --git a/src/restApiInstance.ts b/src/restApiInstance.ts
index 192a562b..760d43dd 100644
--- a/src/restApiInstance.ts
+++ b/src/restApiInstance.ts
@@ -15,7 +15,10 @@ import {
} from './sdks/tableau/interceptors.js';
import RestApi from './sdks/tableau/restApi.js';
import { Server } from './server.js';
+import { TableauAuthInfo } from './server/oauth/schemas.js';
+import { userAgent } from './server/userAgent.js';
import { getExceptionMessage } from './utils/getExceptionMessage.js';
+import invariant from './utils/invariant.js';
import { isAxiosError } from './utils/isAxiosError.js';
type JwtScopes =
@@ -32,8 +35,12 @@ const getNewRestApiInstanceAsync = async (
requestId: RequestId,
server: Server,
jwtScopes: Set,
+ authInfo?: TableauAuthInfo,
): Promise => {
- const restApi = new RestApi(config.server, {
+ const tableauServer = config.server || authInfo?.server;
+ invariant(tableauServer, 'Tableau server could not be determined');
+
+ const restApi = new RestApi(tableauServer, {
requestInterceptor: [
getRequestInterceptor(server, requestId),
getRequestErrorInterceptor(server, requestId),
@@ -55,13 +62,19 @@ const getNewRestApiInstanceAsync = async (
await restApi.signIn({
type: 'direct-trust',
siteName: config.siteName,
- username: getJwtSubClaim(config),
+ username: getJwtSubClaim(config, authInfo),
clientId: config.connectedAppClientId,
secretId: config.connectedAppSecretId,
secretValue: config.connectedAppSecretValue,
scopes: jwtScopes,
- additionalPayload: getJwtAdditionalPayload(config),
+ additionalPayload: getJwtAdditionalPayload(config, authInfo),
});
+ } else {
+ if (!authInfo?.accessToken || !authInfo?.userId) {
+ throw new Error('Auth info is required when not signing in first.');
+ }
+
+ restApi.setCredentials(authInfo.accessToken, authInfo.userId);
}
return restApi;
@@ -73,25 +86,38 @@ export const useRestApi = async ({
server,
callback,
jwtScopes,
+ authInfo,
}: {
config: Config;
requestId: RequestId;
server: Server;
jwtScopes: Array;
callback: (restApi: RestApi) => Promise;
+ authInfo?: TableauAuthInfo;
}): Promise => {
- const restApi = await getNewRestApiInstanceAsync(config, requestId, server, new Set(jwtScopes));
+ const restApi = await getNewRestApiInstanceAsync(
+ config,
+ requestId,
+ server,
+ new Set(jwtScopes),
+ authInfo,
+ );
try {
return await callback(restApi);
} finally {
- await restApi.signOut();
+ if (config.auth !== 'oauth') {
+ // Tableau REST sessions for 'pat' and 'direct-trust' are intentionally ephemeral.
+ // Sessions for 'oauth' are not. Signing out would invalidate the session,
+ // preventing the access token from being reused for subsequent requests.
+ await restApi.signOut();
+ }
}
};
export const getRequestInterceptor =
(server: Server, requestId: RequestId): RequestInterceptor =>
(request) => {
- request.headers['User-Agent'] = `${server.name}/${server.version}`;
+ request.headers['User-Agent'] = userAgent;
logRequest(server, request, requestId);
return request;
};
@@ -197,11 +223,14 @@ function logResponse(
log.info(server, messageObj, { logger: 'rest-api', requestId });
}
-function getJwtSubClaim(config: Config): string {
- return config.jwtSubClaim;
+function getJwtSubClaim(config: Config, authInfo: TableauAuthInfo | undefined): string {
+ return config.jwtSubClaim.replaceAll('{OAUTH_USERNAME}', authInfo?.username ?? '');
}
-function getJwtAdditionalPayload(config: Config): Record {
- const json = config.jwtAdditionalPayload;
+function getJwtAdditionalPayload(
+ config: Config,
+ authInfo: TableauAuthInfo | undefined,
+): Record {
+ const json = config.jwtAdditionalPayload.replaceAll('{OAUTH_USERNAME}', authInfo?.username ?? '');
return JSON.parse(json || '{}');
}
diff --git a/src/scripts/createClaudeMcpBundleManifest.ts b/src/scripts/createClaudeMcpBundleManifest.ts
index 346df838..7aaf783c 100644
--- a/src/scripts/createClaudeMcpBundleManifest.ts
+++ b/src/scripts/createClaudeMcpBundleManifest.ts
@@ -232,6 +232,78 @@ const envVars = {
required: false,
sensitive: false,
},
+ DISABLE_OAUTH: {
+ includeInUserConfig: false,
+ type: 'boolean',
+ title: 'Disable OAuth',
+ description: 'Disable OAuth when transport is http.',
+ required: false,
+ sensitive: false,
+ },
+ OAUTH_ISSUER: {
+ includeInUserConfig: false,
+ type: 'string',
+ title: 'OAuth Issuer',
+ description: 'The OAuth issuer.',
+ required: false,
+ sensitive: false,
+ },
+ OAUTH_JWE_PRIVATE_KEY: {
+ includeInUserConfig: false,
+ type: 'string',
+ title: 'OAuth JWE Private Key',
+ description: 'The OAuth JWE private key.',
+ required: false,
+ sensitive: true,
+ },
+ OAUTH_JWE_PRIVATE_KEY_PATH: {
+ includeInUserConfig: false,
+ type: 'string',
+ title: 'OAuth JWE Private Key Path',
+ description: 'The path to the OAuth JWE private key.',
+ required: false,
+ sensitive: true,
+ },
+ OAUTH_JWE_PRIVATE_KEY_PASSPHRASE: {
+ includeInUserConfig: false,
+ type: 'string',
+ title: 'OAuth JWE Private Key Passphrase',
+ description: 'The passphrase for the OAuth JWE private key.',
+ required: false,
+ sensitive: true,
+ },
+ OAUTH_REDIRECT_URI: {
+ includeInUserConfig: false,
+ type: 'string',
+ title: 'OAuth Redirect URI',
+ description: 'The OAuth redirect URI.',
+ required: false,
+ sensitive: false,
+ },
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: {
+ includeInUserConfig: false,
+ type: 'number',
+ title: 'OAuth Authorization Code Timeout',
+ description: 'The OAuth authorization code timeout.',
+ required: false,
+ sensitive: false,
+ },
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: {
+ includeInUserConfig: false,
+ type: 'number',
+ title: 'OAuth Access Token Timeout',
+ description: 'The OAuth access token timeout.',
+ required: false,
+ sensitive: false,
+ },
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: {
+ includeInUserConfig: false,
+ type: 'number',
+ title: 'OAuth Refresh Token Timeout',
+ description: 'The OAuth refresh token timeout.',
+ required: false,
+ sensitive: false,
+ },
} satisfies EnvVars;
const userConfig = Object.entries(envVars).reduce>(
diff --git a/src/sdks/tableau-oauth/apis.ts b/src/sdks/tableau-oauth/apis.ts
new file mode 100644
index 00000000..c03a77e6
--- /dev/null
+++ b/src/sdks/tableau-oauth/apis.ts
@@ -0,0 +1,19 @@
+import { makeApi } from '@zodios/core';
+
+import { tableauAccessTokenRequestSchema, tableauAccessTokenResponseSchema } from './types.js';
+
+export const tableauTokenApi = makeApi([
+ {
+ method: 'post',
+ path: '/oauth2/v1/token',
+ alias: 'token',
+ response: tableauAccessTokenResponseSchema,
+ parameters: [
+ {
+ name: 'body',
+ type: 'Body',
+ schema: tableauAccessTokenRequestSchema,
+ },
+ ],
+ },
+]);
diff --git a/src/sdks/tableau-oauth/client.ts b/src/sdks/tableau-oauth/client.ts
new file mode 100644
index 00000000..669a5967
--- /dev/null
+++ b/src/sdks/tableau-oauth/client.ts
@@ -0,0 +1,7 @@
+import { Zodios, ZodiosInstance } from '@zodios/core';
+
+import { tableauTokenApi } from './apis.js';
+
+export const getClient = (basePath: string): ZodiosInstance => {
+ return new Zodios(basePath, tableauTokenApi);
+};
diff --git a/src/sdks/tableau-oauth/methods.ts b/src/sdks/tableau-oauth/methods.ts
new file mode 100644
index 00000000..b5226262
--- /dev/null
+++ b/src/sdks/tableau-oauth/methods.ts
@@ -0,0 +1,15 @@
+import { userAgent } from '../../server/userAgent.js';
+import { getClient } from './client.js';
+import { TableauAccessToken, TableauAccessTokenRequest } from './types.js';
+
+export async function getTokenResult(
+ basePath: string,
+ request: TableauAccessTokenRequest,
+): Promise {
+ return await getClient(basePath).token(request, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'User-Agent': userAgent,
+ },
+ });
+}
diff --git a/src/sdks/tableau-oauth/types.ts b/src/sdks/tableau-oauth/types.ts
new file mode 100644
index 00000000..a70d1abc
--- /dev/null
+++ b/src/sdks/tableau-oauth/types.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+
+import { requiredString } from '../../utils/requiredStrings.js';
+
+export const tableauAccessTokenRequestSchema = z.discriminatedUnion('grant_type', [
+ z.object({
+ grant_type: z.literal('authorization_code'),
+ code: z.string(),
+ code_verifier: z.string(),
+ redirect_uri: z.string(),
+ client_id: z.string(),
+ }),
+ z.object({
+ grant_type: z.literal('refresh_token'),
+ client_id: z.string(),
+ refresh_token: z.string(),
+ site_namespace: z.string(),
+ }),
+]);
+
+export const tableauAccessTokenResponseSchema = z
+ .object({
+ access_token: requiredString('access_token'),
+ expires_in: z.number().int().nonnegative(),
+ refresh_token: requiredString('refresh_token'),
+ origin_host: requiredString('origin_host'),
+ })
+ .transform((data) => ({
+ accessToken: data.access_token,
+ expiresInSeconds: data.expires_in,
+ refreshToken: data.refresh_token,
+ originHost: data.origin_host,
+ }));
+
+export type TableauAccessTokenRequest = z.infer;
+export type TableauAccessToken = z.infer;
diff --git a/src/sdks/tableau/apis/serverApi.ts b/src/sdks/tableau/apis/serverApi.ts
new file mode 100644
index 00000000..1d68653a
--- /dev/null
+++ b/src/sdks/tableau/apis/serverApi.ts
@@ -0,0 +1,34 @@
+import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core';
+import { z } from 'zod';
+
+import { siteSchema } from '../types/site.js';
+import { userSchema } from '../types/user.js';
+
+const sessionSchema = z.object({
+ site: siteSchema,
+ user: userSchema,
+});
+
+const getCurrentServerSessionEndpoint = makeEndpoint({
+ method: 'get',
+ path: '/sessions/current',
+ alias: 'getCurrentServerSession',
+ description: 'Returns details of the current session of Tableau Server.',
+ response: z.object({ session: sessionSchema }),
+ errors: [
+ {
+ status: 401,
+ schema: z.object({
+ error: z.object({
+ code: z.string(),
+ summary: z.string(),
+ detail: z.string(),
+ }),
+ }),
+ },
+ ],
+});
+
+export type Session = z.infer;
+const serverApi = makeApi([getCurrentServerSessionEndpoint]);
+export const serverApis = [...serverApi] as const satisfies ZodiosEndpointDefinitions;
diff --git a/src/sdks/tableau/apis/viewsApi.ts b/src/sdks/tableau/apis/viewsApi.ts
index 6857a686..88a290d1 100644
--- a/src/sdks/tableau/apis/viewsApi.ts
+++ b/src/sdks/tableau/apis/viewsApi.ts
@@ -7,7 +7,7 @@ import { paginationParameters } from './paginationParameters.js';
const queryViewDataEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/views/:viewId/data`,
+ path: '/sites/:siteId/views/:viewId/data',
alias: 'queryViewData',
description: 'Returns a specified view rendered as data in comma separated value (CSV) format.',
response: z.string(),
@@ -15,7 +15,7 @@ const queryViewDataEndpoint = makeEndpoint({
const queryViewImageEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/views/:viewId/image`,
+ path: '/sites/:siteId/views/:viewId/image',
alias: 'queryViewImage',
description: 'Returns an image of the specified view.',
parameters: [
@@ -46,7 +46,7 @@ const queryViewImageEndpoint = makeEndpoint({
const queryViewsForWorkbookEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/workbooks/:workbookId/views`,
+ path: '/sites/:siteId/workbooks/:workbookId/views',
alias: 'queryViewsForWorkbook',
description:
'Returns all the views for the specified workbook, optionally including usage statistics.',
@@ -63,7 +63,7 @@ const queryViewsForWorkbookEndpoint = makeEndpoint({
const queryViewsForSiteEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/views`,
+ path: '/sites/:siteId/views',
alias: 'queryViewsForSite',
description:
'Returns all the views for the specified site, optionally including usage statistics.',
diff --git a/src/sdks/tableau/apis/vizqlDataServiceApi.ts b/src/sdks/tableau/apis/vizqlDataServiceApi.ts
index 56d433d6..f7f642c9 100644
--- a/src/sdks/tableau/apis/vizqlDataServiceApi.ts
+++ b/src/sdks/tableau/apis/vizqlDataServiceApi.ts
@@ -282,7 +282,7 @@ const vizqlDataServiceApi = makeApi([
method: 'post',
path: '/query-datasource',
alias: 'queryDatasource',
- description: `Queries a specific data source and returns the resulting data.`,
+ description: 'Queries a specific data source and returns the resulting data.',
requestFormat: 'json',
parameters: [
{
@@ -307,7 +307,8 @@ const vizqlDataServiceApi = makeApi([
method: 'post',
path: '/read-metadata',
alias: 'readMetadata',
- description: `Requests metadata for a specific data source. The metadata provides information about the data fields, such as field names, data types, and descriptions.`,
+ description:
+ 'Requests metadata for a specific data source. The metadata provides information about the data fields, such as field names, data types, and descriptions.',
requestFormat: 'json',
parameters: [
{
@@ -328,7 +329,7 @@ const vizqlDataServiceApi = makeApi([
method: 'get',
path: '/simple-request',
alias: 'simpleRequest',
- description: `Sends a request that can be used for testing or doing a health check.`,
+ description: 'Sends a request that can be used for testing or doing a health check.',
requestFormat: 'json',
response: z.string(),
},
diff --git a/src/sdks/tableau/apis/workbooksApi.ts b/src/sdks/tableau/apis/workbooksApi.ts
index d9bc7d0f..0dacf2fa 100644
--- a/src/sdks/tableau/apis/workbooksApi.ts
+++ b/src/sdks/tableau/apis/workbooksApi.ts
@@ -7,7 +7,7 @@ import { paginationParameters } from './paginationParameters.js';
const getWorkbookEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/workbooks/:workbookId`,
+ path: '/sites/:siteId/workbooks/:workbookId',
alias: 'getWorkbook',
description:
'Returns information about the specified workbook, including information about views and tags.',
@@ -16,7 +16,7 @@ const getWorkbookEndpoint = makeEndpoint({
const queryWorkbooksForSiteEndpoint = makeEndpoint({
method: 'get',
- path: `/sites/:siteId/workbooks`,
+ path: '/sites/:siteId/workbooks',
alias: 'queryWorkbooksForSite',
description: 'Returns the workbooks on a site.',
parameters: [
diff --git a/src/sdks/tableau/methods/authenticationMethods.ts b/src/sdks/tableau/methods/authenticationMethods.ts
index 921fcfdd..5a46d80d 100644
--- a/src/sdks/tableau/methods/authenticationMethods.ts
+++ b/src/sdks/tableau/methods/authenticationMethods.ts
@@ -76,10 +76,11 @@ export class AuthenticatedAuthenticationMethods extends AuthenticatedMethods<
/**
* Signs you out of the current session. This call invalidates the authentication token that is created by a call to Sign In.
- * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_authentication.htm#sign_out
*
* Required scopes: none
*
+ * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_authentication.htm#sign_out
+ *
*/
signOut = async (): Promise => {
await this._apiClient.signOut(undefined, {
diff --git a/src/sdks/tableau/methods/metadataMethods.ts b/src/sdks/tableau/methods/metadataMethods.ts
index 8ace1f2d..8b972c46 100644
--- a/src/sdks/tableau/methods/metadataMethods.ts
+++ b/src/sdks/tableau/methods/metadataMethods.ts
@@ -21,9 +21,8 @@ export default class MetadataMethods extends AuthenticatedMethods => {
return await this._apiClient.graphql({ query }, { ...this.authHeader });
diff --git a/src/sdks/tableau/methods/serverMethods.ts b/src/sdks/tableau/methods/serverMethods.ts
new file mode 100644
index 00000000..0f169ea6
--- /dev/null
+++ b/src/sdks/tableau/methods/serverMethods.ts
@@ -0,0 +1,50 @@
+import { isErrorFromAlias, Zodios } from '@zodios/core';
+import { Err, Ok, Result } from 'ts-results-es';
+
+import { isAxiosError } from '../../../../node_modules/axios/index.js';
+import { getExceptionMessage } from '../../../utils/getExceptionMessage.js';
+import { serverApis, Session } from '../apis/serverApi.js';
+import { Credentials } from '../types/credentials.js';
+import AuthenticatedMethods from './authenticatedMethods.js';
+
+/**
+ * Server methods of the Tableau Server REST API
+ *
+ * @export
+ * @class ServerMethods
+ * @extends {AuthenticatedMethods}
+ * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_server.htm
+ */
+export default class ServerMethods extends AuthenticatedMethods {
+ constructor(baseUrl: string, creds: Credentials) {
+ super(new Zodios(baseUrl, serverApis), creds);
+ }
+
+ /**
+ * Returns details of the current session of Tableau Server.
+ *
+ * Required scopes: none
+ *
+ * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_server.htm#get-current-server-session
+ */
+ getCurrentServerSession = async (): Promise<
+ Result
+ > => {
+ try {
+ const response = await this._apiClient.getCurrentServerSession({
+ ...this.authHeader,
+ });
+ return Ok(response.session);
+ } catch (error) {
+ if (isErrorFromAlias(this._apiClient.api, 'getCurrentServerSession', error)) {
+ return Err({ type: 'unauthorized', message: error.response.data.error });
+ }
+
+ if (isAxiosError(error) && error.response) {
+ return Err({ type: 'unknown', message: error.response.data });
+ }
+
+ return Err({ type: 'unknown', message: getExceptionMessage(error) });
+ }
+ };
+}
diff --git a/src/sdks/tableau/restApi.ts b/src/sdks/tableau/restApi.ts
index b397edc6..be351fa7 100644
--- a/src/sdks/tableau/restApi.ts
+++ b/src/sdks/tableau/restApi.ts
@@ -14,6 +14,7 @@ import ContentExplorationMethods from './methods/contentExplorationMethods.js';
import DatasourcesMethods from './methods/datasourcesMethods.js';
import MetadataMethods from './methods/metadataMethods.js';
import PulseMethods from './methods/pulseMethods.js';
+import ServerMethods from './methods/serverMethods.js';
import ViewsMethods from './methods/viewsMethods.js';
import VizqlDataServiceMethods from './methods/vizqlDataServiceMethods.js';
import WorkbooksMethods from './methods/workbooksMethods.js';
@@ -36,6 +37,7 @@ export default class RestApi {
private _contentExplorationMethods?: ContentExplorationMethods;
private _datasourcesMethods?: DatasourcesMethods;
private _metadataMethods?: MetadataMethods;
+ private _serverMethods?: ServerMethods;
private _pulseMethods?: PulseMethods;
private _vizqlDataServiceMethods?: VizqlDataServiceMethods;
private _viewsMethods?: ViewsMethods;
@@ -67,6 +69,24 @@ export default class RestApi {
return this._creds;
}
+ setCredentials(accessToken: string, userId: string): void {
+ const parts = accessToken.split('|');
+ if (parts.length < 3) {
+ throw new Error('Could not determine site ID. Access token must have 3 parts.');
+ }
+
+ const siteId = parts[2];
+ this._creds = {
+ site: {
+ id: siteId,
+ },
+ user: {
+ id: userId,
+ },
+ token: accessToken,
+ };
+ }
+
get siteId(): string {
return this.creds.site.id;
}
@@ -124,6 +144,15 @@ export default class RestApi {
return this._metadataMethods;
}
+ get serverMethods(): ServerMethods {
+ if (!this._serverMethods) {
+ this._serverMethods = new ServerMethods(this._baseUrl, this.creds);
+ this._addInterceptors(this._baseUrl, this._serverMethods.interceptors);
+ }
+
+ return this._serverMethods;
+ }
+
get pulseMethods(): PulseMethods {
if (!this._pulseMethods) {
this._pulseMethods = new PulseMethods(this._baseUrlWithoutVersion, this.creds);
diff --git a/src/sdks/tableau/types/site.ts b/src/sdks/tableau/types/site.ts
new file mode 100644
index 00000000..00db1c8a
--- /dev/null
+++ b/src/sdks/tableau/types/site.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod';
+
+export const siteSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ // Add other fields as needed
+});
diff --git a/src/sdks/tableau/types/user.ts b/src/sdks/tableau/types/user.ts
new file mode 100644
index 00000000..4c0f7f45
--- /dev/null
+++ b/src/sdks/tableau/types/user.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+export const userSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ // Add other fields as needed
+});
+
+export type User = z.infer;
diff --git a/src/server/express.ts b/src/server/express.ts
index 97a53703..90a5c51e 100644
--- a/src/server/express.ts
+++ b/src/server/express.ts
@@ -1,7 +1,7 @@
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
import cors from 'cors';
-import express, { Request, Response } from 'express';
+import express, { Request, RequestHandler, Response } from 'express';
import fs, { existsSync } from 'fs';
import http from 'http';
import https from 'https';
@@ -9,6 +9,8 @@ import https from 'https';
import { Config } from '../config.js';
import { setLogLevel } from '../logging/log.js';
import { Server } from '../server.js';
+import { validateProtocolVersion } from './middleware.js';
+import { OAuthProvider } from './oauth/provider.js';
export async function startExpressServer({
basePath,
@@ -39,10 +41,18 @@ export async function startExpressServer({
}),
);
+ const middleware: Array = [];
+ if (config.oauth.enabled) {
+ const oauthProvider = new OAuthProvider();
+ oauthProvider.setupRoutes(app);
+ middleware.push(oauthProvider.authMiddleware);
+ middleware.push(validateProtocolVersion);
+ }
+
const path = `/${basePath}`;
- app.post(path, createMcpServer);
- app.get(path, methodNotAllowed);
- app.delete(path, methodNotAllowed);
+ app.post(path, ...middleware, createMcpServer);
+ app.get(path, ...middleware, methodNotAllowed);
+ app.delete(path, ...middleware, methodNotAllowed);
const useSsl = !!(config.sslKey && config.sslCert);
if (!useSsl) {
@@ -79,7 +89,7 @@ export async function startExpressServer({
async function createMcpServer(req: Request, res: Response): Promise {
try {
const server = new Server();
- const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
@@ -96,7 +106,6 @@ export async function startExpressServer({
await transport.handleRequest(req, res, req.body);
} catch (error) {
- // eslint-disable-next-line no-console -- console.error is intentional here since the transport is not stdio.
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
diff --git a/src/server/middleware.ts b/src/server/middleware.ts
new file mode 100644
index 00000000..9426e639
--- /dev/null
+++ b/src/server/middleware.ts
@@ -0,0 +1,31 @@
+import { NextFunction, Request, Response } from 'express';
+
+/**
+ * Validate MCP protocol version
+ */
+export function validateProtocolVersion(req: Request, res: Response, next: NextFunction): void {
+ const version = req.headers['mcp-protocol-version'];
+
+ // If no version header, continue (backwards compatibility)
+ if (!version) {
+ next();
+ return;
+ }
+
+ // Check supported versions
+ const supportedVersions = ['2025-06-18', '2025-03-26', '2024-11-05'];
+ if (!supportedVersions.includes(version as string)) {
+ res.status(400).json({
+ jsonrpc: '2.0',
+ error: {
+ code: -32600,
+ message: 'Unsupported protocol version',
+ data: { supported: supportedVersions, requested: version },
+ },
+ id: null,
+ });
+ return;
+ }
+
+ next();
+}
diff --git a/src/server/oauth/.well-known/oauth-authorization-server.ts b/src/server/oauth/.well-known/oauth-authorization-server.ts
new file mode 100644
index 00000000..ca21a3d9
--- /dev/null
+++ b/src/server/oauth/.well-known/oauth-authorization-server.ts
@@ -0,0 +1,30 @@
+import express from 'express';
+
+import { getConfig } from '../../../config.js';
+
+/**
+ * OAuth 2.0 Authorization Server Metadata
+ *
+ * @remarks
+ * MCP OAuth Step 3: Authorization Server Metadata
+ *
+ * Returns metadata about the authorization server including
+ * available endpoints, supported flows, and capabilities.
+ */
+export function oauthAuthorizationServer(app: express.Application): void {
+ app.get('/.well-known/oauth-authorization-server', (req, res) => {
+ const origin = getConfig().oauth.issuer;
+ res.json({
+ issuer: origin,
+ authorization_endpoint: `${origin}/oauth/authorize`,
+ token_endpoint: `${origin}/oauth/token`,
+ registration_endpoint: `${origin}/oauth/register`,
+ response_types_supported: ['code'],
+ grant_types_supported: ['authorization_code', 'refresh_token'],
+ code_challenge_methods_supported: ['S256'],
+ scopes_supported: ['mcp:tools:tableau:read'],
+ token_endpoint_auth_methods_supported: ['none'],
+ subject_types_supported: ['public'],
+ });
+ });
+}
diff --git a/src/server/oauth/.well-known/oauth-protected-resource.ts b/src/server/oauth/.well-known/oauth-protected-resource.ts
new file mode 100644
index 00000000..46bf5855
--- /dev/null
+++ b/src/server/oauth/.well-known/oauth-protected-resource.ts
@@ -0,0 +1,25 @@
+import express from 'express';
+
+import { getConfig } from '../../../config.js';
+import { serverName } from '../../../server.js';
+
+export function oauthProtectedResource(app: express.Application): void {
+ /**
+ * OAuth 2.0 Protected Resource Metadata
+ *
+ * @remarks
+ * MCP OAuth Step 2: Resource Metadata Discovery
+ *
+ * Returns metadata about the protected resource and its
+ * authorization servers. Client discovers this URL from
+ * WWW-Authenticate header in 401 response.
+ */
+ app.get('/.well-known/oauth-protected-resource', (_req, res) => {
+ const issuer = getConfig().oauth.issuer;
+ res.json({
+ resource: `${issuer}/${serverName}`,
+ authorization_servers: [issuer],
+ bearer_methods_supported: ['header'],
+ });
+ });
+}
diff --git a/src/server/oauth/authMiddleware.ts b/src/server/oauth/authMiddleware.ts
new file mode 100644
index 00000000..fddb0c3e
--- /dev/null
+++ b/src/server/oauth/authMiddleware.ts
@@ -0,0 +1,179 @@
+import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
+import { KeyObject } from 'crypto';
+import express, { RequestHandler } from 'express';
+import { compactDecrypt } from 'jose';
+import { Err, Ok, Result } from 'ts-results-es';
+
+import { getConfig } from '../../config.js';
+import { AUDIENCE } from './provider.js';
+import { mcpAccessTokenSchema, mcpAccessTokenUserOnlySchema, TableauAuthInfo } from './schemas.js';
+import { AuthenticatedRequest } from './types.js';
+
+/**
+ * Express middleware for OAuth authentication
+ *
+ * @remarks
+ * MCP OAuth Step 1: Initial Request (401 Unauthorized)
+ *
+ * This middleware checks for Bearer token authorization.
+ * If no token is present, returns 401 with WWW-Authenticate header
+ * pointing to resource metadata endpoint.
+ *
+ * @returns Express middleware function
+ */
+export function authMiddleware(privateKey: KeyObject): RequestHandler {
+ return async (
+ req: AuthenticatedRequest,
+ res: express.Response,
+ next: express.NextFunction,
+ ): Promise => {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ // For SSE requests (GET), provide proper SSE error response
+ if (req.method === 'GET' && req.headers.accept?.includes('text/event-stream')) {
+ res.writeHead(401, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ });
+ res.write('event: error\n');
+ res.write(
+ 'data: {"error": "unauthorized", "error_description": "Authorization required"}\n\n',
+ );
+ res.end();
+ return;
+ }
+
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
+ res
+ .status(401)
+ .header(
+ 'WWW-Authenticate',
+ `Bearer realm="MCP", resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`,
+ )
+ .json({
+ error: 'unauthorized',
+ error_description: 'Authorization required. Use OAuth 2.1 flow.',
+ });
+ return;
+ }
+
+ const token = authHeader.slice(7);
+ const result = await verifyAccessToken(token, privateKey);
+
+ if (result.isErr()) {
+ // For SSE requests (GET), provide proper SSE error response
+ if (req.method === 'GET' && req.headers.accept?.includes('text/event-stream')) {
+ res.writeHead(401, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ });
+ res.write('event: error\n');
+ res.write(`data: {"error": "invalid_token", "error_description": "${result.error}"}\n\n`);
+ res.end();
+ return;
+ }
+
+ res.status(401).json({
+ error: 'invalid_token',
+ error_description: result.error,
+ });
+ return;
+ }
+ req.auth = result.value;
+ next();
+ };
+}
+
+/**
+ * Verifies JWE access token and extracts credentials
+ *
+ * @remarks
+ * MCP OAuth Step 8: Authenticated MCP Request
+ *
+ * Decrypts and validates JWE signature and expiration.
+ * Extracts access/refresh tokens for API calls.
+ *
+ * @param token - JWT access token from Authorization header
+ * @returns AuthInfo with user details and tokens
+ */
+async function verifyAccessToken(
+ token: string,
+ jwePrivateKey: KeyObject,
+): Promise> {
+ const config = getConfig();
+ const privateKey = jwePrivateKey;
+ try {
+ const { plaintext } = await compactDecrypt(token, privateKey);
+ const payload = JSON.parse(new TextDecoder().decode(plaintext));
+
+ if (
+ !payload ||
+ payload.iss !== config.oauth.issuer ||
+ payload.aud !== AUDIENCE ||
+ !payload.exp ||
+ payload.exp < Date.now()
+ ) {
+ // https://github.com/modelcontextprotocol/inspector/issues/608
+ // MCP Inspector Not Using Refresh Token for Token Validation
+ return new Err('Invalid or expired access token');
+ }
+
+ let authInfo: TableauAuthInfo;
+ if (config.auth === 'oauth') {
+ const mcpAccessToken = mcpAccessTokenSchema.safeParse(payload);
+ if (!mcpAccessToken.success) {
+ return Err(
+ `Invalid access token: ${mcpAccessToken.error.errors.map((e) => e.message).join(', ')}`,
+ );
+ }
+
+ const {
+ tableauAccessToken,
+ tableauRefreshToken,
+ tableauExpiresAt,
+ tableauUserId,
+ tableauServer,
+ sub,
+ } = mcpAccessToken.data;
+
+ if (Date.now() > tableauExpiresAt) {
+ return new Err('Invalid or expired access token');
+ }
+
+ authInfo = {
+ username: sub,
+ userId: tableauUserId,
+ server: tableauServer,
+ accessToken: tableauAccessToken,
+ refreshToken: tableauRefreshToken,
+ };
+ } else {
+ const mcpAccessToken = mcpAccessTokenUserOnlySchema.safeParse(payload);
+ if (!mcpAccessToken.success) {
+ return Err(
+ `Invalid access token: ${mcpAccessToken.error.errors.map((e) => e.message).join(', ')}`,
+ );
+ }
+
+ const { tableauUserId, tableauServer, sub } = mcpAccessToken.data;
+ authInfo = {
+ username: sub,
+ userId: tableauUserId,
+ server: tableauServer,
+ };
+ }
+
+ return Ok({
+ token,
+ clientId: 'mcp-client',
+ scopes: ['read'],
+ expiresAt: payload.exp,
+ extra: authInfo,
+ });
+ } catch {
+ return new Err('Invalid or expired access token');
+ }
+}
diff --git a/src/server/oauth/authorize.ts b/src/server/oauth/authorize.ts
new file mode 100644
index 00000000..17d57d6f
--- /dev/null
+++ b/src/server/oauth/authorize.ts
@@ -0,0 +1,151 @@
+import { randomBytes, randomUUID } from 'crypto';
+import express from 'express';
+
+import { getConfig } from '../../config.js';
+import { generateCodeChallenge } from './generateCodeChallenge.js';
+import { TABLEAU_CLOUD_SERVER_URL } from './provider.js';
+import { mcpAuthorizeSchema } from './schemas.js';
+import { PendingAuthorization } from './types.js';
+
+export function authorize(
+ app: express.Application,
+ pendingAuthorizations: Map,
+): void {
+ const config = getConfig();
+
+ /**
+ * OAuth 2.1 Authorization Endpoint
+ *
+ * @remarks
+ * MCP OAuth Step 5: Authorization Request with PKCE
+ *
+ * Handles authorization requests with PKCE parameters.
+ * Validates request, stores pending authorization, and
+ * redirects to OAuth for user consent.
+ */
+ app.get('/oauth/authorize', (req, res) => {
+ const result = mcpAuthorizeSchema.safeParse(req.query);
+
+ if (!result.success) {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: result.error.errors.map((e) => e.message).join(', '),
+ });
+ return;
+ }
+
+ const {
+ clientId,
+ redirectUri,
+ responseType,
+ codeChallenge,
+ codeChallengeMethod,
+ state,
+ scope = 'read',
+ } = result.data;
+
+ if (responseType !== 'code') {
+ res.status(400).json({
+ error: 'unsupported_response_type',
+ error_description: 'Only authorization code flow is supported',
+ });
+ return;
+ }
+
+ if (codeChallengeMethod !== 'S256') {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: 'Only S256 code challenge method is supported',
+ });
+ return;
+ }
+
+ // Validate redirect URI using security rules (for public clients)
+ try {
+ const url = new URL(redirectUri);
+
+ // Allow HTTPS URLs
+ if (url.protocol === 'https:') {
+ // HTTPS is always allowed
+ }
+ // Allow HTTP only for localhost
+ else if (
+ url.protocol === 'http:' &&
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1')
+ ) {
+ // Localhost HTTP is allowed
+ }
+ // Allow custom schemes (like systemprompt://)
+ else if (url.protocol.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:$/)) {
+ // Custom schemes are allowed
+ } else {
+ throw new Error('Invalid protocol');
+ }
+ } catch {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: 'Invalid redirect URI: must use HTTPS, localhost HTTP, or custom scheme',
+ });
+ return;
+ }
+
+ // Generate Tableau state and store pending authorization
+ const tableauState = randomBytes(32).toString('hex');
+ const authKey = randomBytes(32).toString('hex');
+
+ const tableauClientId = randomUUID();
+ pendingAuthorizations.set(authKey, {
+ clientId,
+ redirectUri,
+ codeChallenge,
+ codeChallengeMethod,
+ state: state ?? '',
+ scope,
+ tableauState,
+ tableauClientId,
+ });
+
+ // Clean up expired authorizations
+ setTimeout(() => pendingAuthorizations.delete(authKey), config.oauth.authzCodeTimeoutMs);
+
+ // Redirect to Tableau OAuth
+ const server = config.server || TABLEAU_CLOUD_SERVER_URL;
+ const tableauCodeChallenge = generateCodeChallenge(codeChallenge);
+ const oauthUrl = new URL(`${server}/oauth2/v1/auth`);
+ oauthUrl.searchParams.set('client_id', tableauClientId);
+ oauthUrl.searchParams.set('code_challenge', tableauCodeChallenge);
+ oauthUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
+ oauthUrl.searchParams.set('response_type', 'code');
+ oauthUrl.searchParams.set('redirect_uri', config.oauth.redirectUri);
+ oauthUrl.searchParams.set('state', `${authKey}:${tableauState}`);
+ oauthUrl.searchParams.set('device_id', randomUUID());
+ oauthUrl.searchParams.set('target_site', config.siteName);
+ oauthUrl.searchParams.set('device_name', getDeviceName(redirectUri, state ?? ''));
+ oauthUrl.searchParams.set('client_type', 'tableau-mcp');
+
+ res.redirect(oauthUrl.toString());
+ });
+}
+
+function getDeviceName(redirectUri: string, state: string): string {
+ const defaultDeviceName = 'tableau-mcp (Unknown agent)';
+
+ try {
+ const url = new URL(redirectUri);
+ if (url.protocol === 'https:' || url.protocol === 'http:') {
+ if (redirectUri === 'https://vscode.dev/redirect' && new URL(state).protocol === 'vscode:') {
+ // VS Code normally authenticates in a way that doesn't give any clues about who it is.
+ // It has a backup authentication method they call "URL Handler" that does though.
+ return 'tableau-mcp (VS Code)';
+ }
+
+ return defaultDeviceName;
+ } else if (url.protocol === 'cursor:') {
+ return 'tableau-mcp (Cursor)';
+ } else {
+ return `tableau-mcp (${url.protocol.slice(0, -1)})`;
+ }
+ } catch {
+ return defaultDeviceName;
+ }
+}
diff --git a/src/server/oauth/callback.ts b/src/server/oauth/callback.ts
new file mode 100644
index 00000000..94cfd31c
--- /dev/null
+++ b/src/server/oauth/callback.ts
@@ -0,0 +1,199 @@
+import { randomBytes } from 'crypto';
+import express from 'express';
+import { Err, Ok, Result } from 'ts-results-es';
+
+import { getConfig } from '../../config.js';
+import RestApi from '../../sdks/tableau/restApi.js';
+import { getTokenResult } from '../../sdks/tableau-oauth/methods.js';
+import { TableauAccessToken } from '../../sdks/tableau-oauth/types.js';
+import { getExceptionMessage } from '../../utils/getExceptionMessage.js';
+import { isAxiosError } from '../../utils/isAxiosError.js';
+import { TABLEAU_CLOUD_SERVER_URL } from './provider.js';
+import { callbackSchema } from './schemas.js';
+import { AuthorizationCode, PendingAuthorization } from './types.js';
+
+export function callback(
+ app: express.Application,
+ pendingAuthorizations: Map,
+ authorizationCodes: Map,
+): void {
+ const config = getConfig();
+
+ /**
+ * OAuth Callback Handler
+ *
+ * @remarks
+ * MCP OAuth Step 6: OAuth Callback
+ *
+ * Receives callback from after user authorization.
+ * Exchanges code for tokens, generates MCP authorization
+ * code, and redirects back to client with code.
+ */
+ app.get('/Callback', async (req, res) => {
+ const result = callbackSchema.safeParse(req.query);
+
+ if (!result.success) {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: result.error.errors.map((e) => e.message).join(', '),
+ });
+ return;
+ }
+
+ const { code, state, error } = result.data;
+ if (error) {
+ res.status(400).json({
+ error: 'access_denied',
+ error_description: 'User denied authorization',
+ });
+ return;
+ }
+
+ try {
+ // Parse state to get auth key and Tableau state
+ const [authKey, tableauState] = state.split(':');
+ const pendingAuth = pendingAuthorizations.get(authKey);
+
+ if (!pendingAuth || pendingAuth.tableauState !== tableauState) {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: 'Invalid state parameter',
+ });
+ return;
+ }
+
+ const tokensResult = await exchangeAuthorizationCode({
+ server: config.server || TABLEAU_CLOUD_SERVER_URL,
+ code,
+ redirectUri: config.oauth.redirectUri,
+ clientId: pendingAuth.tableauClientId,
+ codeVerifier: pendingAuth.codeChallenge,
+ });
+
+ if (tokensResult.isErr()) {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: tokensResult.error,
+ });
+ return;
+ }
+
+ const { accessToken, refreshToken, expiresInSeconds, originHost } = tokensResult.value;
+ const originHostUrl = new URL(`https://${originHost}`);
+
+ if (config.server) {
+ const configServerUrl = new URL(config.server);
+ if (originHostUrl.hostname !== configServerUrl.hostname) {
+ // Not sure if this can actually happen but without returning an error here,
+ // this would fail downstream when attempting to authenticate to the REST API.
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: `Invalid origin host: ${originHost}. Expected: ${config.server}`,
+ });
+ return;
+ }
+ }
+
+ const server = originHostUrl.toString();
+ const restApi = new RestApi(server);
+ restApi.setCredentials(accessToken, 'unknown user id');
+ const sessionResult = await restApi.serverMethods.getCurrentServerSession();
+ if (sessionResult.isErr()) {
+ if (sessionResult.error.type === 'unauthorized') {
+ res.status(401).json({
+ error: 'unauthorized',
+ error_description: `Unable to get the Tableau server session. Error: ${JSON.stringify(sessionResult.error)}`,
+ });
+ } else {
+ res.status(500).json({
+ error: 'server_error',
+ error_description:
+ 'Internal server error during authorization. Unable to get the Tableau server session. Contact your administrator.',
+ });
+ }
+ return;
+ }
+
+ // Generate authorization code
+ const authorizationCode = randomBytes(32).toString('hex');
+ authorizationCodes.set(authorizationCode, {
+ clientId: pendingAuth.clientId,
+ redirectUri: pendingAuth.redirectUri,
+ codeChallenge: pendingAuth.codeChallenge,
+ user: sessionResult.value.user,
+ server,
+ tableauClientId: pendingAuth.tableauClientId,
+ tokens: {
+ accessToken,
+ refreshToken,
+ expiresInSeconds,
+ },
+ expiresAt: Date.now() + config.oauth.authzCodeTimeoutMs,
+ });
+
+ // Clean up
+ pendingAuthorizations.delete(authKey);
+
+ // Redirect back to client with authorization code
+ const redirectUrl = new URL(pendingAuth.redirectUri);
+ redirectUrl.searchParams.set('code', authorizationCode);
+ redirectUrl.searchParams.set('state', pendingAuth.state);
+
+ res.redirect(redirectUrl.toString());
+ } catch (error) {
+ console.error('OAuth callback error:', error);
+ res.status(500).json({
+ error: 'server_error',
+ error_description:
+ 'Internal server error during authorization. Contact your administrator.',
+ });
+ }
+ });
+}
+
+/**
+ * Exchanges authorization code for access tokens
+ *
+ * @remarks
+ * Part of MCP OAuth Step 6: OAuth Callback
+ * Uses token endpoint with basic auth
+ *
+ * @param server - Tableau server host
+ * @param code - Authorization code
+ * @param redirectUri - Redirect URI used in initial request
+ * @param clientId - Client ID
+ * @param codeVerifier - Code verifier
+ * @returns token response with access_token and refresh_token
+ */
+async function exchangeAuthorizationCode({
+ server,
+ code,
+ redirectUri,
+ clientId,
+ codeVerifier,
+}: {
+ server: string;
+ code: string;
+ redirectUri: string;
+ clientId: string;
+ codeVerifier: string;
+}): Promise> {
+ try {
+ const result = await getTokenResult(server, {
+ grant_type: 'authorization_code',
+ code,
+ redirect_uri: redirectUri,
+ client_id: clientId,
+ code_verifier: codeVerifier,
+ });
+
+ return Ok(result);
+ } catch (error) {
+ if (!isAxiosError(error) || !error.response) {
+ return Err(`Failed to exchange authorization code: ${getExceptionMessage(error)}`);
+ }
+
+ const errorText = JSON.stringify(error.response.data);
+ return Err(`Failed to exchange authorization code: ${error.response.status} - ${errorText}`);
+ }
+}
diff --git a/src/server/oauth/generateCodeChallenge.ts b/src/server/oauth/generateCodeChallenge.ts
new file mode 100644
index 00000000..44069db7
--- /dev/null
+++ b/src/server/oauth/generateCodeChallenge.ts
@@ -0,0 +1,14 @@
+import { createHash } from 'crypto';
+
+/**
+ * Generates PKCE code challenge from verifier
+ *
+ * @remarks
+ * Uses SHA256 hashing as required by S256 method
+ *
+ * @param verifier - Random code verifier string
+ * @returns Base64url-encoded SHA256 hash of verifier
+ */
+export function generateCodeChallenge(verifier: string): string {
+ return createHash('sha256').update(verifier).digest('base64url');
+}
diff --git a/src/server/oauth/provider.ts b/src/server/oauth/provider.ts
new file mode 100644
index 00000000..83bfee6c
--- /dev/null
+++ b/src/server/oauth/provider.ts
@@ -0,0 +1,99 @@
+import { createPrivateKey, createPublicKey, KeyObject } from 'crypto';
+import express, { RequestHandler } from 'express';
+import { readFileSync } from 'fs';
+
+import { getConfig } from '../../config.js';
+import { oauthAuthorizationServer } from './.well-known/oauth-authorization-server.js';
+import { oauthProtectedResource } from './.well-known/oauth-protected-resource.js';
+import { authMiddleware } from './authMiddleware.js';
+import { authorize } from './authorize.js';
+import { callback } from './callback.js';
+import { register } from './register.js';
+import { token } from './token.js';
+import { AuthorizationCode, PendingAuthorization, RefreshTokenData } from './types.js';
+
+export const TABLEAU_CLOUD_SERVER_URL = 'https://online.tableau.com';
+export const AUDIENCE = 'tableau-mcp-server';
+
+/**
+ * OAuth 2.1 Provider
+ *
+ * @remarks
+ * Implements the complete MCP OAuth 2.1 flow with PKCE
+ * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization
+ *
+ * This provider handles:
+ * - Step 1: Initial 401 response with WWW-Authenticate
+ * - Step 2: Resource metadata discovery
+ * - Step 3: Authorization server metadata
+ * - Step 4: Dynamic client registration
+ * - Step 5: Authorization with PKCE
+ * - Step 6: OAuth callback
+ * - Step 7: Token exchange
+ * - Step 8: Authenticated requests
+ *
+ * Security features:
+ * - PKCE (RFC 7636) for authorization code flow
+ * - Secure state parameter validation
+ * - Time-limited authorization codes
+ */
+export class OAuthProvider {
+ private readonly config = getConfig();
+
+ private readonly pendingAuthorizations = new Map();
+ private readonly authorizationCodes = new Map();
+ private readonly refreshTokens = new Map();
+
+ private readonly privateKey: KeyObject;
+ private readonly publicKey: KeyObject;
+
+ constructor() {
+ this.privateKey = this.getPrivateKey();
+ this.publicKey = createPublicKey(this.privateKey);
+ }
+
+ get authMiddleware(): RequestHandler {
+ return authMiddleware(this.privateKey);
+ }
+
+ setupRoutes(app: express.Application): void {
+ // .well-known/oauth-authorization-server
+ oauthAuthorizationServer(app);
+
+ // .well-known/oauth-protected-resource
+ oauthProtectedResource(app);
+
+ // oauth/register
+ register(app);
+
+ // oauth/authorize
+ authorize(app, this.pendingAuthorizations);
+
+ // /Callback
+ callback(app, this.pendingAuthorizations, this.authorizationCodes);
+
+ // oauth/token
+ token(app, this.authorizationCodes, this.refreshTokens, this.publicKey);
+ }
+
+ private getPrivateKey(): KeyObject {
+ let privateKeyContents = this.config.oauth.jwePrivateKey.replace(/\\n/g, '\n');
+ if (!privateKeyContents) {
+ try {
+ privateKeyContents = readFileSync(this.config.oauth.jwePrivateKeyPath, 'utf8');
+ } catch (e) {
+ throw new Error(`Failed to read private key file: ${e}`);
+ }
+ }
+
+ try {
+ return createPrivateKey({
+ key: privateKeyContents,
+ format: 'pem',
+ passphrase: this.config.oauth.jwePrivateKeyPassphrase,
+ });
+ } catch (e) {
+ throw new Error(`Failed to create private key: ${e}`);
+ }
+ }
+}
diff --git a/src/server/oauth/register.ts b/src/server/oauth/register.ts
new file mode 100644
index 00000000..c09a95d7
--- /dev/null
+++ b/src/server/oauth/register.ts
@@ -0,0 +1,72 @@
+import express from 'express';
+
+export function register(app: express.Application): void {
+ /**
+ * Dynamic Client Registration Endpoint
+ *
+ * @remarks
+ * MCP OAuth Step 4: Dynamic Client Registration (Optional)
+ *
+ * Allows clients to dynamically register with the authorization
+ * server. For public clients (like desktop apps), no client
+ * secret is required - security comes from PKCE.
+ */
+ app.post('/oauth/register', express.json(), (req, res) => {
+ const { redirect_uris } = req.body;
+
+ const validatedRedirectUris = [];
+ if (redirect_uris && Array.isArray(redirect_uris)) {
+ for (const uri of redirect_uris) {
+ if (typeof uri !== 'string') {
+ res.status(400).json({
+ error: 'invalid_redirect_uri',
+ error_description: 'redirect_uris must be an array of strings',
+ });
+ return;
+ }
+
+ // Validate using same security rules as authorization endpoint
+ try {
+ const url = new URL(uri);
+
+ if (url.protocol === 'https:') {
+ // Allow HTTPS URLs
+ validatedRedirectUris.push(uri);
+ } else if (
+ url.protocol === 'http:' &&
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1')
+ ) {
+ // Allow HTTP only for localhost
+ validatedRedirectUris.push(uri);
+ } else if (url.protocol.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:$/)) {
+ // Allow custom schemes
+ validatedRedirectUris.push(uri);
+ } else {
+ res.status(400).json({
+ error: 'invalid_redirect_uri',
+ error_description: `Invalid redirect URI: ${uri}. Must use HTTPS, localhost HTTP, or custom scheme`,
+ });
+ return;
+ }
+ } catch {
+ res.status(400).json({
+ error: 'invalid_redirect_uri',
+ error_description: `Invalid redirect URI format: ${uri}`,
+ });
+ return;
+ }
+ }
+ }
+
+ // For public clients, we use a fixed client ID since no authentication is required
+ // The security comes from PKCE (code challenge/verifier) at authorization time
+ res.json({
+ client_id: 'mcp-public-client',
+ redirect_uris: validatedRedirectUris,
+ grant_types: ['authorization_code'],
+ response_types: ['code'],
+ token_endpoint_auth_method: 'none',
+ application_type: 'native',
+ });
+ });
+}
diff --git a/src/server/oauth/schemas.ts b/src/server/oauth/schemas.ts
new file mode 100644
index 00000000..4612032b
--- /dev/null
+++ b/src/server/oauth/schemas.ts
@@ -0,0 +1,112 @@
+import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
+import { z } from 'zod';
+
+import { requiredString } from '../../utils/requiredStrings.js';
+
+export const mcpAuthorizeSchema = z
+ .object({
+ client_id: requiredString('client_id'),
+ redirect_uri: requiredString('redirect_uri'),
+ response_type: requiredString('response_type'),
+ code_challenge: requiredString('code_challenge'),
+ code_challenge_method: requiredString('code_challenge_method'),
+ state: z.string().optional(),
+ scope: z.string().optional(),
+ })
+ .transform((data) => ({
+ clientId: data.client_id,
+ redirectUri: data.redirect_uri,
+ responseType: data.response_type,
+ codeChallenge: data.code_challenge,
+ codeChallengeMethod: data.code_challenge_method,
+ state: data.state,
+ scope: data.scope,
+ }));
+
+export const mcpTokenSchema = z
+ .discriminatedUnion(
+ 'grant_type',
+ [
+ z.object({
+ grant_type: z.literal('authorization_code'),
+ code: requiredString('code'),
+ redirect_uri: requiredString('redirect_uri'),
+ code_verifier: requiredString('code_verifier'),
+ client_id: requiredString('client_id'),
+ }),
+ z.object({
+ grant_type: z.literal('refresh_token'),
+ refresh_token: requiredString('refresh_token'),
+ }),
+ ],
+ {
+ errorMap: (issue, ctx) => ({
+ message:
+ issue.code === 'invalid_union_discriminator'
+ ? `grant_type must be ${issue.options.map((opt) => `'${String(opt)}'`).join(' | ')}, got '${ctx.data.grant_type}'.`
+ : ctx.defaultError,
+ }),
+ },
+ )
+ .transform((data) => {
+ if (data.grant_type === 'authorization_code') {
+ return {
+ grantType: data.grant_type,
+ code: data.code,
+ redirectUri: data.redirect_uri,
+ codeVerifier: data.code_verifier,
+ clientId: data.client_id,
+ };
+ }
+
+ return {
+ grantType: data.grant_type,
+ refreshToken: data.refresh_token,
+ };
+ });
+
+export const callbackSchema = z.object({
+ code: requiredString('code'),
+ state: requiredString('state'),
+ error: z.string().optional(),
+});
+
+export const mcpAccessTokenUserOnlySchema = z.object({
+ sub: requiredString('sub'),
+ tableauServer: requiredString('tableauServer'),
+ tableauUserId: requiredString('tableauUserId'),
+});
+
+export const mcpAccessTokenSchema = mcpAccessTokenUserOnlySchema.extend({
+ tableauAccessToken: requiredString('tableauAccessToken'),
+ tableauRefreshToken: requiredString('tableauRefreshToken'),
+ tableauExpiresAt: z.number().int().nonnegative(),
+});
+
+export type McpAccessToken = z.infer;
+export type McpAccessTokenSubOnly = z.infer;
+
+export const tableauAuthInfoSchema = z
+ .object({
+ username: z.string(),
+ userId: z.string(),
+ server: z.string(),
+ accessToken: z.string(),
+ refreshToken: z.string(),
+ })
+ .partial();
+
+export type TableauAuthInfo = z.infer;
+
+export const getTableauAuthInfo = (authInfo: AuthInfo | undefined): TableauAuthInfo | undefined => {
+ if (!authInfo) {
+ return;
+ }
+
+ const tableauAuthInfo = tableauAuthInfoSchema.safeParse(authInfo.extra);
+ if (!tableauAuthInfo.success) {
+ return;
+ }
+
+ return tableauAuthInfo.data;
+};
diff --git a/src/server/oauth/token.ts b/src/server/oauth/token.ts
new file mode 100644
index 00000000..8059fcfc
--- /dev/null
+++ b/src/server/oauth/token.ts
@@ -0,0 +1,221 @@
+import { KeyObject, randomBytes } from 'crypto';
+import express from 'express';
+import { CompactEncrypt } from 'jose';
+import { Err, Ok, Result } from 'ts-results-es';
+
+import { getConfig } from '../../config.js';
+import { getTokenResult } from '../../sdks/tableau-oauth/methods.js';
+import { TableauAccessToken } from '../../sdks/tableau-oauth/types.js';
+import { getExceptionMessage } from '../../utils/getExceptionMessage.js';
+import { isAxiosError } from '../../utils/isAxiosError.js';
+import { generateCodeChallenge } from './generateCodeChallenge.js';
+import { AUDIENCE } from './provider.js';
+import { mcpTokenSchema } from './schemas.js';
+import { AuthorizationCode, RefreshTokenData, UserAndTokens } from './types.js';
+
+export function token(
+ app: express.Application,
+ authorizationCodes: Map,
+ refreshTokens: Map,
+ publicKey: KeyObject,
+): void {
+ const config = getConfig();
+
+ /**
+ * OAuth 2.1 Token Endpoint
+ *
+ * @remarks
+ * MCP OAuth Step 7: Token Exchange with PKCE Verification
+ *
+ * Exchanges authorization code for access token.
+ * Verifies PKCE code_verifier matches the original challenge.
+ * Returns JWT containing tokens for API access.
+ */
+ app.post('/oauth/token', async (req, res) => {
+ const result = mcpTokenSchema.safeParse(req.body);
+
+ if (!result.success) {
+ res.status(400).json({
+ error: 'invalid_request',
+ error_description: result.error.errors.map((e) => e.message).join(', '),
+ });
+ return;
+ }
+
+ const { grantType } = result.data;
+
+ try {
+ if (grantType === 'authorization_code') {
+ // Handle authorization code exchange
+ const { code, codeVerifier } = result.data;
+ const authCode = authorizationCodes.get(code);
+ if (!authCode || authCode.expiresAt < Date.now()) {
+ authorizationCodes.delete(code);
+ res.status(400).json({
+ error: 'invalid_grant',
+ error_description: 'Invalid or expired authorization code',
+ });
+ return;
+ }
+
+ // Verify PKCE
+ const challengeFromVerifier = generateCodeChallenge(codeVerifier);
+ if (challengeFromVerifier !== authCode.codeChallenge) {
+ res.status(400).json({
+ error: 'invalid_grant',
+ error_description: 'Invalid code verifier',
+ });
+ return;
+ }
+
+ // Generate tokens
+ const refreshTokenId = randomBytes(32).toString('hex');
+ const accessToken = await createAccessToken(authCode, publicKey);
+ refreshTokens.set(refreshTokenId, {
+ user: authCode.user,
+ server: authCode.server,
+ clientId: authCode.clientId,
+ tokens: authCode.tokens,
+ expiresAt: Date.now() + config.oauth.refreshTokenTimeoutMs,
+ tableauClientId: authCode.tableauClientId,
+ });
+
+ setTimeout(() => refreshTokens.delete(refreshTokenId), config.oauth.refreshTokenTimeoutMs);
+
+ authorizationCodes.delete(code);
+
+ res.json({
+ access_token: accessToken,
+ token_type: 'Bearer',
+ expires_in: config.oauth.accessTokenTimeoutMs / 1000,
+ refresh_token: refreshTokenId,
+ scope: 'read',
+ });
+ return;
+ } else {
+ // Handle refresh token
+ const { refreshToken } = result.data;
+ const tokenData = refreshTokens.get(refreshToken);
+ if (!tokenData || tokenData.expiresAt < Date.now()) {
+ // Refresh token is expired
+ refreshTokens.delete(refreshToken);
+ res.status(400).json({
+ error: 'invalid_grant',
+ error_description: 'Invalid or expired refresh token',
+ });
+ return;
+ }
+
+ const { refreshToken: tableauRefreshToken } = tokenData.tokens;
+
+ const tokensResult = await exchangeRefreshToken(
+ tokenData.server,
+ tableauRefreshToken,
+ tokenData.tableauClientId,
+ );
+
+ let accessToken: string;
+ if (tokensResult.isErr()) {
+ // If the refresh token exchange fails, reuse the existing Tableau access token
+ // which may nor may not be expired.
+ accessToken = await createAccessToken(tokenData, publicKey);
+ } else {
+ const {
+ accessToken: newTableauAccessToken,
+ refreshToken: newTableauRefreshToken,
+ expiresInSeconds,
+ } = tokensResult.value;
+
+ accessToken = await createAccessToken(
+ {
+ user: tokenData.user,
+ server: tokenData.server,
+ tokens: {
+ accessToken: newTableauAccessToken,
+ refreshToken: newTableauRefreshToken,
+ expiresInSeconds,
+ },
+ },
+ publicKey,
+ );
+ }
+
+ res.json({
+ access_token: accessToken,
+ token_type: 'Bearer',
+ expires_in: config.oauth.accessTokenTimeoutMs / 1000,
+ scope: 'read',
+ });
+ return;
+ }
+ } catch (error) {
+ console.error('Token endpoint error:', error);
+ res.status(500).json({
+ error: 'server_error',
+ error_description: 'Internal server error',
+ });
+ return;
+ }
+ });
+}
+
+/**
+ * Creates JWE access token containing credentials
+ *
+ * @remarks
+ * Part of MCP OAuth Step 7: Token Exchange
+ * JWE contains tokens for making API calls
+ *
+ * @param tokenData - token data
+ * @returns Encrypted JWE token for MCP authentication
+ */
+async function createAccessToken(tokenData: UserAndTokens, publicKey: KeyObject): Promise {
+ const config = getConfig();
+
+ const payload = JSON.stringify({
+ sub: tokenData.user.name,
+ tableauServer: tokenData.server,
+ tableauUserId: tokenData.user.id,
+ iat: Math.floor(Date.now() / 1000),
+ exp: Date.now() + config.oauth.accessTokenTimeoutMs,
+ aud: AUDIENCE,
+ iss: config.oauth.issuer,
+ ...(config.auth === 'oauth'
+ ? {
+ tableauAccessToken: tokenData.tokens.accessToken,
+ tableauRefreshToken: tokenData.tokens.refreshToken,
+ tableauExpiresAt: Date.now() + tokenData.tokens.expiresInSeconds * 1000,
+ }
+ : {}),
+ });
+
+ const jwe = await new CompactEncrypt(new TextEncoder().encode(payload))
+ .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
+ .encrypt(publicKey);
+
+ return jwe;
+}
+
+async function exchangeRefreshToken(
+ server: string,
+ refreshToken: string,
+ clientId: string,
+): Promise> {
+ try {
+ const result = await getTokenResult(server, {
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: clientId,
+ site_namespace: '',
+ });
+
+ return Ok(result);
+ } catch (error) {
+ if (!isAxiosError(error) || !error.response) {
+ return Err(`Failed to exchange refresh token: ${getExceptionMessage(error)}`);
+ }
+
+ const errorText = JSON.stringify(error.response.data);
+ return Err(`Failed to exchange refresh token: ${error.response.status} - ${errorText}`);
+ }
+}
diff --git a/src/server/oauth/types.ts b/src/server/oauth/types.ts
new file mode 100644
index 00000000..4317dbdb
--- /dev/null
+++ b/src/server/oauth/types.ts
@@ -0,0 +1,45 @@
+import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
+import express from 'express';
+
+import { User } from '../../sdks/tableau/types/user.js';
+
+export type AuthenticatedRequest = express.Request & {
+ auth?: AuthInfo;
+};
+
+export type Tokens = {
+ accessToken: string;
+ refreshToken: string;
+ expiresInSeconds: number;
+};
+
+export type PendingAuthorization = {
+ clientId: string;
+ redirectUri: string;
+ codeChallenge: string;
+ codeChallengeMethod: string;
+ state: string;
+ scope: string;
+ tableauState: string;
+ tableauClientId: string;
+};
+
+export type UserAndTokens = {
+ user: User;
+ server: string;
+ tokens: Tokens;
+};
+
+export type AuthorizationCode = UserAndTokens & {
+ clientId: string;
+ redirectUri: string;
+ codeChallenge: string;
+ expiresAt: number;
+ tableauClientId: string;
+};
+
+export type RefreshTokenData = UserAndTokens & {
+ clientId: string;
+ expiresAt: number;
+ tableauClientId: string;
+};
diff --git a/src/server/userAgent.ts b/src/server/userAgent.ts
new file mode 100644
index 00000000..3ff44a7c
--- /dev/null
+++ b/src/server/userAgent.ts
@@ -0,0 +1,3 @@
+import pkg from '../../package.json' with { type: 'json' };
+
+export const userAgent = `${pkg.name}/${pkg.version}`;
diff --git a/src/tools/contentExploration/searchContent.ts b/src/tools/contentExploration/searchContent.ts
index 4c22ef92..b1f34a34 100644
--- a/src/tools/contentExploration/searchContent.ts
+++ b/src/tools/contentExploration/searchContent.ts
@@ -9,6 +9,7 @@ import {
searchContentFilterSchema,
} from '../../sdks/tableau/types/contentExploration.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { Tool } from '../tool.js';
import {
buildFilterString,
@@ -58,12 +59,16 @@ This tool searches across all supported content types for objects relevant to th
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ terms, limit, orderBy, filter }, { requestId }): Promise => {
+ callback: async (
+ { terms, limit, orderBy, filter },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
const orderByString = orderBy ? buildOrderByString(orderBy) : undefined;
const filterString = filter ? buildFilterString(filter) : undefined;
return await searchContentTool.logAndExecute({
requestId,
+ authInfo,
args: {},
callback: async () => {
return new Ok(
@@ -72,6 +77,7 @@ This tool searches across all supported content types for objects relevant to th
requestId,
server,
jwtScopes: ['tableau:content:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
const response = await restApi.contentExplorationMethods.searchContent({
terms,
diff --git a/src/tools/getDatasourceMetadata/getDatasourceMetadata.ts b/src/tools/getDatasourceMetadata/getDatasourceMetadata.ts
index a6b062f0..748c08ba 100644
--- a/src/tools/getDatasourceMetadata/getDatasourceMetadata.ts
+++ b/src/tools/getDatasourceMetadata/getDatasourceMetadata.ts
@@ -6,6 +6,7 @@ import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { GraphQLResponse } from '../../sdks/tableau/apis/metadataApi.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { getVizqlDataServiceDisabledError } from '../getVizqlDataServiceDisabledError.js';
import { Tool } from '../tool.js';
import { validateDatasourceLuid } from '../validateDatasourceLuid.js';
@@ -102,7 +103,7 @@ export const getGetDatasourceMetadataTool = (server: Server): Tool => {
+ callback: async ({ datasourceLuid }, { requestId, authInfo }): Promise => {
const config = getConfig();
const query = getGraphqlQuery(datasourceLuid);
@@ -111,6 +112,7 @@ export const getGetDatasourceMetadataTool = (server: Server): Tool({
requestId,
+ authInfo,
args: { datasourceLuid },
callback: async () => {
return await useRestApi({
@@ -118,6 +120,7 @@ export const getGetDatasourceMetadataTool = (server: Server): Tool {
// Fetching metadata from VizQL Data Service API.
const readMetadataResult = await restApi.vizqlDataServiceMethods.readMetadata({
diff --git a/src/tools/listDatasources/listDatasources.ts b/src/tools/listDatasources/listDatasources.ts
index b80b9701..e164ea6a 100644
--- a/src/tools/listDatasources/listDatasources.ts
+++ b/src/tools/listDatasources/listDatasources.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { paginate } from '../../utils/paginate.js';
import { genericFilterDescription } from '../genericFilterDescription.js';
import { Tool } from '../tool.js';
@@ -76,11 +77,15 @@ export const getListDatasourcesTool = (server: Server): Tool => {
+ callback: async (
+ { filter, pageSize, limit },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
const validatedFilter = filter ? parseAndValidateDatasourcesFilterString(filter) : undefined;
return await listDatasourcesTool.logAndExecute({
requestId,
+ authInfo,
args: { filter, pageSize, limit },
callback: async () => {
const datasources = await useRestApi({
@@ -88,6 +93,7 @@ export const getListDatasourcesTool = (server: Server): Tool {
const datasources = await paginate({
pageConfig: {
diff --git a/src/tools/pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.ts b/src/tools/pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.ts
index ba77968a..3e3c2aa9 100644
--- a/src/tools/pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.ts
+++ b/src/tools/pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.ts
@@ -8,6 +8,7 @@ import {
pulseInsightBundleTypeEnum,
} from '../../../sdks/tableau/types/pulse.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -136,10 +137,14 @@ Generate an insight bundle for the current aggregated value for Pulse Metric usi
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ bundleRequest, bundleType }, { requestId }): Promise => {
+ callback: async (
+ { bundleRequest, bundleType },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
return await generatePulseMetricValueInsightBundleTool.logAndExecute({
requestId,
+ authInfo,
args: { bundleRequest, bundleType },
callback: async () => {
return await useRestApi({
@@ -147,6 +152,7 @@ Generate an insight bundle for the current aggregated value for Pulse Metric usi
requestId,
server,
jwtScopes: ['tableau:insights:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) =>
await restApi.pulseMethods.generatePulseMetricValueInsightBundle(
bundleRequest,
diff --git a/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.test.ts b/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.test.ts
index 95336d8b..89502d73 100644
--- a/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.test.ts
+++ b/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.test.ts
@@ -36,6 +36,7 @@ vi.mock('../../../restApiInstance.js', () => ({
pulseMethods: {
listAllPulseMetricDefinitions: mocks.mockListAllPulseMetricDefinitions,
},
+ siteId: 'test-site-id',
}),
),
}));
diff --git a/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.ts b/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.ts
index f174a2eb..73d64ad6 100644
--- a/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.ts
+++ b/src/tools/pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.ts
@@ -5,6 +5,7 @@ import { getConfig } from '../../../config.js';
import { useRestApi } from '../../../restApiInstance.js';
import { pulseMetricDefinitionViewEnum } from '../../../sdks/tableau/types/pulse.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -44,10 +45,11 @@ Retrieves a list of all published Pulse Metric Definitions using the Tableau RES
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ view }, { requestId }): Promise => {
+ callback: async ({ view }, { requestId, authInfo }): Promise => {
const config = getConfig();
return await listAllPulseMetricDefinitionsTool.logAndExecute({
requestId,
+ authInfo,
args: { view },
callback: async () => {
return await useRestApi({
@@ -55,6 +57,7 @@ Retrieves a list of all published Pulse Metric Definitions using the Tableau RES
requestId,
server,
jwtScopes: ['tableau:insight_definitions_metrics:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.pulseMethods.listAllPulseMetricDefinitions(view);
},
diff --git a/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.test.ts b/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.test.ts
index 1740ed11..03eed63b 100644
--- a/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.test.ts
+++ b/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.test.ts
@@ -37,6 +37,7 @@ vi.mock('../../../restApiInstance.js', () => ({
listPulseMetricDefinitionsFromMetricDefinitionIds:
mocks.mockListPulseMetricDefinitionsFromMetricDefinitionIds,
},
+ siteId: 'test-site-id',
}),
),
}));
diff --git a/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.ts b/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.ts
index b9e06e2d..d49763f2 100644
--- a/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.ts
+++ b/src/tools/pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.ts
@@ -5,6 +5,7 @@ import { getConfig } from '../../../config.js';
import { useRestApi } from '../../../restApiInstance.js';
import { pulseMetricDefinitionViewEnum } from '../../../sdks/tableau/types/pulse.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -55,10 +56,14 @@ Retrieves a list of specific Pulse Metric Definitions using the Tableau REST API
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ view, metricDefinitionIds }, { requestId }): Promise => {
+ callback: async (
+ { view, metricDefinitionIds },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
return await listPulseMetricDefinitionsFromDefinitionIdsTool.logAndExecute({
requestId,
+ authInfo,
args: { metricDefinitionIds, view },
callback: async () => {
return await useRestApi({
@@ -66,6 +71,7 @@ Retrieves a list of specific Pulse Metric Definitions using the Tableau REST API
requestId,
server,
jwtScopes: ['tableau:insight_definitions_metrics:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.pulseMethods.listPulseMetricDefinitionsFromMetricDefinitionIds(
metricDefinitionIds,
diff --git a/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.test.ts b/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.test.ts
index f9dee4ca..70d3d196 100644
--- a/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.test.ts
+++ b/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.test.ts
@@ -21,6 +21,7 @@ vi.mock('../../../restApiInstance.js', () => ({
listPulseMetricSubscriptionsForCurrentUser:
mocks.mockListPulseMetricSubscriptionsForCurrentUser,
},
+ siteId: 'test-site-id',
}),
),
}));
diff --git a/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.ts b/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.ts
index a2ba878a..c5035145 100644
--- a/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.ts
+++ b/src/tools/pulse/listMetricSubscriptions/listPulseMetricSubscriptions.ts
@@ -3,6 +3,7 @@ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { getConfig } from '../../../config.js';
import { useRestApi } from '../../../restApiInstance.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -30,10 +31,11 @@ Retrieves a list of published Pulse Metric Subscriptions for the current user us
readOnlyHint: true,
openWorldHint: false,
},
- callback: async (_, { requestId }): Promise => {
+ callback: async (_, { requestId, authInfo }): Promise => {
const config = getConfig();
return await listPulseMetricSubscriptionsTool.logAndExecute({
requestId,
+ authInfo,
args: {},
callback: async () => {
return await useRestApi({
@@ -41,6 +43,7 @@ Retrieves a list of published Pulse Metric Subscriptions for the current user us
requestId,
server,
jwtScopes: ['tableau:metric_subscriptions:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.pulseMethods.listPulseMetricSubscriptionsForCurrentUser();
},
diff --git a/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.test.ts b/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.test.ts
index dce20404..cd0b44e7 100644
--- a/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.test.ts
+++ b/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.test.ts
@@ -28,6 +28,7 @@ vi.mock('../../../restApiInstance.js', () => ({
pulseMethods: {
listPulseMetricsFromMetricDefinitionId: mocks.mockListPulseMetricsFromMetricDefinitionId,
},
+ siteId: 'test-site-id',
}),
),
}));
diff --git a/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.ts b/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.ts
index 81da4e1f..e0eada3e 100644
--- a/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.ts
+++ b/src/tools/pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.ts
@@ -4,6 +4,7 @@ import { z } from 'zod';
import { getConfig } from '../../../config.js';
import { useRestApi } from '../../../restApiInstance.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -32,10 +33,14 @@ Retrieves a list of published Pulse Metrics from a Pulse Metric Definition using
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ pulseMetricDefinitionID }, { requestId }): Promise => {
+ callback: async (
+ { pulseMetricDefinitionID },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
return await listPulseMetricsFromMetricDefinitionIdTool.logAndExecute({
requestId,
+ authInfo,
args: { pulseMetricDefinitionID },
callback: async () => {
return await useRestApi({
@@ -43,6 +48,7 @@ Retrieves a list of published Pulse Metrics from a Pulse Metric Definition using
requestId,
server,
jwtScopes: ['tableau:insight_definitions_metrics:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.pulseMethods.listPulseMetricsFromMetricDefinitionId(
pulseMetricDefinitionID,
diff --git a/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.test.ts b/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.test.ts
index e00b256b..b47aaece 100644
--- a/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.test.ts
+++ b/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.test.ts
@@ -28,6 +28,7 @@ vi.mock('../../../restApiInstance.js', () => ({
pulseMethods: {
listPulseMetricsFromMetricIds: mocks.mockListPulseMetricsFromMetricIds,
},
+ siteId: 'test-site-id',
}),
),
}));
diff --git a/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.ts b/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.ts
index 5968bf9c..0e9f9584 100644
--- a/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.ts
+++ b/src/tools/pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.ts
@@ -4,6 +4,7 @@ import { z } from 'zod';
import { getConfig } from '../../../config.js';
import { useRestApi } from '../../../restApiInstance.js';
import { Server } from '../../../server.js';
+import { getTableauAuthInfo } from '../../../server/oauth/schemas.js';
import { Tool } from '../../tool.js';
import { getPulseDisabledError } from '../getPulseDisabledError.js';
@@ -36,10 +37,11 @@ Retrieves a list of published Pulse Metrics from a list of metric IDs using the
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ metricIds }, { requestId }): Promise => {
+ callback: async ({ metricIds }, { requestId, authInfo }): Promise => {
const config = getConfig();
return await listPulseMetricsFromMetricIdsTool.logAndExecute({
requestId,
+ authInfo,
args: { metricIds },
callback: async () => {
return await useRestApi({
@@ -47,6 +49,7 @@ Retrieves a list of published Pulse Metrics from a list of metric IDs using the
requestId,
server,
jwtScopes: ['tableau:insight_metrics:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.pulseMethods.listPulseMetricsFromMetricIds(metricIds);
},
diff --git a/src/tools/queryDatasource/queryDatasource.ts b/src/tools/queryDatasource/queryDatasource.ts
index 40dcbfae..c555deb8 100644
--- a/src/tools/queryDatasource/queryDatasource.ts
+++ b/src/tools/queryDatasource/queryDatasource.ts
@@ -12,6 +12,7 @@ import {
TableauError,
} from '../../sdks/tableau/apis/vizqlDataServiceApi.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { getVizqlDataServiceDisabledError } from '../getVizqlDataServiceDisabledError.js';
import { Tool } from '../tool.js';
import { getDatasourceCredentials } from './datasourceCredentials.js';
@@ -52,10 +53,14 @@ export const getQueryDatasourceTool = (server: Server): Tool => {
+ callback: async (
+ { datasourceLuid, query },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
return await queryDatasourceTool.logAndExecute({
requestId,
+ authInfo,
args: { datasourceLuid, query },
callback: async () => {
const datasource: Datasource = { datasourceLuid };
@@ -81,6 +86,7 @@ export const getQueryDatasourceTool = (server: Server): Tool {
if (!config.disableQueryDatasourceFilterValidation) {
// Validate filters values for SET and MATCH filters
diff --git a/src/tools/queryDatasource/queryDatasourceValidator.ts b/src/tools/queryDatasource/queryDatasourceValidator.ts
index 7c58ea17..0ba51903 100644
--- a/src/tools/queryDatasource/queryDatasourceValidator.ts
+++ b/src/tools/queryDatasource/queryDatasourceValidator.ts
@@ -23,6 +23,6 @@ export function validateQuery({
const result = Query.safeParse(query);
if (!result.success) {
- throw new Error(`The query does not match the expected schema.`);
+ throw new Error('The query does not match the expected schema.');
}
}
diff --git a/src/tools/queryDatasource/validators/validateFields.ts b/src/tools/queryDatasource/validators/validateFields.ts
index 2719e5a6..3a732d1c 100644
--- a/src/tools/queryDatasource/validators/validateFields.ts
+++ b/src/tools/queryDatasource/validators/validateFields.ts
@@ -16,7 +16,7 @@ export function validateFields(fields: Fields): void {
{
// Field caption must be a non-empty string.
if (fields.some(hasEmptyFieldCaption)) {
- throw new Error(`The query must not include any fields with an empty fieldCaption.`);
+ throw new Error('The query must not include any fields with an empty fieldCaption.');
}
}
diff --git a/src/tools/queryDatasource/validators/validateFilterValues.ts b/src/tools/queryDatasource/validators/validateFilterValues.ts
index 213d6c1a..a1e6d8fb 100644
--- a/src/tools/queryDatasource/validators/validateFilterValues.ts
+++ b/src/tools/queryDatasource/validators/validateFilterValues.ts
@@ -267,7 +267,7 @@ async function validateMatchFilter(
`Filter validation failed for field "${fieldCaption}". ` +
`No values found that ${patternDescriptions.join(' and ')}. ` +
`${similarValuesString} ` +
- `Please evaluate whether you included the wrong filter value or if you are trying to filter on the wrong field entirely.`;
+ 'Please evaluate whether you included the wrong filter value or if you are trying to filter on the wrong field entirely.';
return new Err({
field: fieldCaption,
diff --git a/src/tools/queryDatasource/validators/validateFilters.ts b/src/tools/queryDatasource/validators/validateFilters.ts
index 43fba9cf..b9a6a922 100644
--- a/src/tools/queryDatasource/validators/validateFilters.ts
+++ b/src/tools/queryDatasource/validators/validateFilters.ts
@@ -10,7 +10,7 @@ export function validateFilters(filters: Query['filters']): void {
if (filters.some((filter) => !('field' in filter))) {
throw new Error(
- `The query must not include filters with invalid fields. The following field errors occurred: The filter must include a field property.`,
+ 'The query must not include filters with invalid fields. The following field errors occurred: The filter must include a field property.',
);
}
@@ -72,7 +72,7 @@ export function validateFilters(filters: Query['filters']): void {
if (setFiltersWithFunctionsOrCalculations.length > 0) {
throw new Error(
- `The query must not include Set Filters, Match Filters, or Relative Date Filters with functions or calculations.`,
+ 'The query must not include Set Filters, Match Filters, or Relative Date Filters with functions or calculations.',
);
}
}
@@ -86,7 +86,7 @@ export function validateFilters(filters: Query['filters']): void {
});
if (setFiltersWithEmptyValues.length > 0) {
- throw new Error(`The query must not include Set Filters with an empty values array.`);
+ throw new Error('The query must not include Set Filters with an empty values array.');
}
}
}
@@ -112,7 +112,7 @@ export function validateFilters(filters: Query['filters']): void {
if (quantitativeDateFiltersWithInvalidDates.length > 0) {
throw new Error(
- `The query must not include Quantitative Date Filters with invalid dates. Dates must use the RFC 3339 standard. Example: 2025-03-14`,
+ 'The query must not include Quantitative Date Filters with invalid dates. Dates must use the RFC 3339 standard. Example: 2025-03-14',
);
}
}
@@ -130,7 +130,7 @@ export function validateFilters(filters: Query['filters']): void {
if (relativeDateFiltersWithInvalidDates.length > 0) {
throw new Error(
- `The query must not include Relative Date Filters with invalid anchor dates. Anchor dates must use the RFC 3339 standard. Example: 2025-03-14`,
+ 'The query must not include Relative Date Filters with invalid anchor dates. Anchor dates must use the RFC 3339 standard. Example: 2025-03-14',
);
}
}
@@ -192,7 +192,7 @@ function validateFilterField(field: FilterField): Result {
{
// Field caption must be a non-empty string.
if (hasEmptyFieldCaption(field)) {
- return new Err(`The fieldCaption property must be a non-empty string.`);
+ return new Err('The fieldCaption property must be a non-empty string.');
}
}
diff --git a/src/tools/tool.test.ts b/src/tools/tool.test.ts
index b466407a..b30f359d 100644
--- a/src/tools/tool.test.ts
+++ b/src/tools/tool.test.ts
@@ -38,12 +38,13 @@ describe('Tool', () => {
const tool = new Tool(mockParams);
const testArgs = { param1: 'test' };
- tool.logInvocation({ requestId: '2', args: testArgs });
+ tool.logInvocation({ requestId: '2', args: testArgs, username: 'test-user' });
const server = expect.any(Object);
expect(spy).toHaveBeenCalledExactlyOnceWith(server, {
type: 'tool',
requestId: '2',
+ username: 'test-user',
tool: {
name: 'get-datasource-metadata',
args: testArgs,
@@ -61,6 +62,7 @@ describe('Tool', () => {
const spy = vi.spyOn(tool, 'logInvocation');
const result = await tool.logAndExecute({
requestId: '2',
+ authInfo: undefined,
args: { param1: 'test' },
callback,
});
@@ -86,6 +88,7 @@ describe('Tool', () => {
const result = await tool.logAndExecute({
requestId: '2',
+ authInfo: undefined,
args: { param1: 'test' },
callback,
});
@@ -101,6 +104,7 @@ describe('Tool', () => {
await tool.logAndExecute({
requestId: '2',
+ authInfo: undefined,
args,
callback: vi.fn(),
});
@@ -128,6 +132,7 @@ describe('Tool', () => {
const result = await tool.logAndExecute({
requestId: '2',
+ authInfo: undefined,
args: { param1: 'test' },
callback: () => Promise.resolve(Ok('test')),
});
diff --git a/src/tools/tool.ts b/src/tools/tool.ts
index f8805dc0..0c17db79 100644
--- a/src/tools/tool.ts
+++ b/src/tools/tool.ts
@@ -1,3 +1,4 @@
+import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CallToolResult, RequestId, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
import { ZodiosError } from '@zodios/core';
@@ -7,6 +8,7 @@ import { fromError, isZodErrorLike } from 'zod-validation-error';
import { getToolLogMessage, log } from '../logging/log.js';
import { Server } from '../server.js';
+import { tableauAuthInfoSchema } from '../server/oauth/schemas.js';
import { getExceptionMessage } from '../utils/getExceptionMessage.js';
import { ToolName } from './toolName.js';
@@ -53,6 +55,9 @@ type LogAndExecuteParams
// The request ID of the tool call
requestId: RequestId;
+ // The Authentication info provided when OAuth is enabled
+ authInfo: AuthInfo | undefined;
+
// The arguments of the tool call
args: Args extends ZodRawShape ? z.objectOutputType : undefined;
@@ -99,8 +104,24 @@ export class Tool {
this.callback = callback;
}
- logInvocation({ requestId, args }: { requestId: RequestId; args: unknown }): void {
- log.debug(this.server, getToolLogMessage({ requestId, toolName: this.name, args }));
+ logInvocation({
+ requestId,
+ args,
+ username,
+ }: {
+ requestId: RequestId;
+ args: unknown;
+ username?: string;
+ }): void {
+ log.debug(
+ this.server,
+ getToolLogMessage({
+ requestId,
+ toolName: this.name,
+ args,
+ username,
+ }),
+ );
}
// Overload for E = undefined (getErrorText omitted)
@@ -122,11 +143,16 @@ export class Tool {
async logAndExecute({
requestId,
args,
+ authInfo,
callback,
getSuccessResult,
getErrorText,
}: LogAndExecuteParams): Promise {
- this.logInvocation({ requestId, args });
+ const username = authInfo?.extra
+ ? tableauAuthInfoSchema.safeParse(authInfo.extra).data?.username
+ : undefined;
+
+ this.logInvocation({ requestId, args, username });
if (args) {
try {
diff --git a/src/tools/views/getViewData.ts b/src/tools/views/getViewData.ts
index d4a0a724..79768e3e 100644
--- a/src/tools/views/getViewData.ts
+++ b/src/tools/views/getViewData.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { Tool } from '../tool.js';
const paramsSchema = {
@@ -15,18 +16,20 @@ export const getGetViewDataTool = (server: Server): Tool =>
const getViewDataTool = new Tool({
server,
name: 'get-view-data',
- description: `Retrieves data in comma separated value (CSV) format for the specified view in a Tableau workbook.`,
+ description:
+ 'Retrieves data in comma separated value (CSV) format for the specified view in a Tableau workbook.',
paramsSchema,
annotations: {
title: 'Get View Data',
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ viewId }, { requestId }): Promise => {
+ callback: async ({ viewId }, { requestId, authInfo }): Promise => {
const config = getConfig();
return await getViewDataTool.logAndExecute({
requestId,
+ authInfo,
args: { viewId },
callback: async () => {
return new Ok(
@@ -35,6 +38,7 @@ export const getGetViewDataTool = (server: Server): Tool =>
requestId,
server,
jwtScopes: ['tableau:views:download'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.viewsMethods.queryViewData({
viewId,
diff --git a/src/tools/views/getViewImage.ts b/src/tools/views/getViewImage.ts
index 41fa1b0e..ec2e5819 100644
--- a/src/tools/views/getViewImage.ts
+++ b/src/tools/views/getViewImage.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { convertPngDataToToolResult } from '../convertPngDataToToolResult.js';
import { Tool } from '../tool.js';
@@ -18,18 +19,23 @@ export const getGetViewImageTool = (server: Server): Tool =
const getViewImageTool = new Tool({
server,
name: 'get-view-image',
- description: `Retrieves an image of the specified view in a Tableau workbook. The width and height in pixels can be provided. The default width and height are both 800 pixels.`,
+ description:
+ 'Retrieves an image of the specified view in a Tableau workbook. The width and height in pixels can be provided. The default width and height are both 800 pixels.',
paramsSchema,
annotations: {
title: 'Get View Image',
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ viewId, width, height }, { requestId }): Promise => {
+ callback: async (
+ { viewId, width, height },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
return await getViewImageTool.logAndExecute({
requestId,
+ authInfo,
args: { viewId },
callback: async () => {
return new Ok(
@@ -38,6 +44,7 @@ export const getGetViewImageTool = (server: Server): Tool =
requestId,
server,
jwtScopes: ['tableau:views:download'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
return await restApi.viewsMethods.queryViewImage({
viewId,
diff --git a/src/tools/views/listViews.ts b/src/tools/views/listViews.ts
index 7b960b89..db2cc404 100644
--- a/src/tools/views/listViews.ts
+++ b/src/tools/views/listViews.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { paginate } from '../../utils/paginate.js';
import { genericFilterDescription } from '../genericFilterDescription.js';
import { Tool } from '../tool.js';
@@ -64,12 +65,16 @@ export const getListViewsTool = (server: Server): Tool => {
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ filter, pageSize, limit }, { requestId }): Promise => {
+ callback: async (
+ { filter, pageSize, limit },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
const validatedFilter = filter ? parseAndValidateViewsFilterString(filter) : undefined;
return await listViewsTool.logAndExecute({
requestId,
+ authInfo,
args: {},
callback: async () => {
return new Ok(
@@ -78,6 +83,7 @@ export const getListViewsTool = (server: Server): Tool => {
requestId,
server,
jwtScopes: ['tableau:content:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
const workbooks = await paginate({
pageConfig: {
diff --git a/src/tools/workbooks/getWorkbook.ts b/src/tools/workbooks/getWorkbook.ts
index 024028b7..4c8be71e 100644
--- a/src/tools/workbooks/getWorkbook.ts
+++ b/src/tools/workbooks/getWorkbook.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { Tool } from '../tool.js';
const paramsSchema = {
@@ -15,18 +16,20 @@ export const getGetWorkbookTool = (server: Server): Tool =>
const getWorkbookTool = new Tool({
server,
name: 'get-workbook',
- description: `Retrieves information about the specified workbook, including information about the views contained in the workbook.`,
+ description:
+ 'Retrieves information about the specified workbook, including information about the views contained in the workbook.',
paramsSchema,
annotations: {
title: 'Get Workbook',
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ workbookId }, { requestId }): Promise => {
+ callback: async ({ workbookId }, { requestId, authInfo }): Promise => {
const config = getConfig();
return await getWorkbookTool.logAndExecute({
requestId,
+ authInfo,
args: { workbookId },
callback: async () => {
return new Ok(
@@ -35,6 +38,7 @@ export const getGetWorkbookTool = (server: Server): Tool =>
requestId,
server,
jwtScopes: ['tableau:content:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
const workbook = await restApi.workbooksMethods.getWorkbook({
workbookId,
diff --git a/src/tools/workbooks/listWorkbooks.ts b/src/tools/workbooks/listWorkbooks.ts
index c00d6d7c..6461e207 100644
--- a/src/tools/workbooks/listWorkbooks.ts
+++ b/src/tools/workbooks/listWorkbooks.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { Server } from '../../server.js';
+import { getTableauAuthInfo } from '../../server/oauth/schemas.js';
import { paginate } from '../../utils/paginate.js';
import { genericFilterDescription } from '../genericFilterDescription.js';
import { Tool } from '../tool.js';
@@ -61,12 +62,16 @@ export const getListWorkbooksTool = (server: Server): Tool
readOnlyHint: true,
openWorldHint: false,
},
- callback: async ({ filter, pageSize, limit }, { requestId }): Promise => {
+ callback: async (
+ { filter, pageSize, limit },
+ { requestId, authInfo },
+ ): Promise => {
const config = getConfig();
const validatedFilter = filter ? parseAndValidateWorkbooksFilterString(filter) : undefined;
return await listWorkbooksTool.logAndExecute({
requestId,
+ authInfo,
args: {},
callback: async () => {
return new Ok(
@@ -75,6 +80,7 @@ export const getListWorkbooksTool = (server: Server): Tool
requestId,
server,
jwtScopes: ['tableau:content:read'],
+ authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
const workbooks = await paginate({
pageConfig: {
diff --git a/src/utils/parseAndValidateFilterString.test.ts b/src/utils/parseAndValidateFilterString.test.ts
index 71e9d65b..d593514c 100644
--- a/src/utils/parseAndValidateFilterString.test.ts
+++ b/src/utils/parseAndValidateFilterString.test.ts
@@ -45,7 +45,7 @@ describe('parseAndValidateFilterString', () => {
filterFieldSchema: FilterFieldSchema,
}),
).toThrowError(
- `Invalid enum value. Expected 'name' | 'projectName' | 'createdAt' | 'updatedAt', received 'notAField'`,
+ "Invalid enum value. Expected 'name' | 'projectName' | 'createdAt' | 'updatedAt', received 'notAField'",
);
});
@@ -57,7 +57,7 @@ describe('parseAndValidateFilterString', () => {
filterFieldSchema: FilterFieldSchema,
}),
).toThrowError(
- `Invalid enum value. Expected 'eq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte', received 'badop'`,
+ "Invalid enum value. Expected 'eq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte', received 'badop'",
);
});
@@ -68,7 +68,7 @@ describe('parseAndValidateFilterString', () => {
allowedOperatorsByField,
filterFieldSchema: FilterFieldSchema,
}),
- ).toThrowError(`Operator 'gt' is not allowed for field 'name'. Allowed operators: eq`);
+ ).toThrowError("Operator 'gt' is not allowed for field 'name'. Allowed operators: eq");
});
it('throws on invalid format', () => {
@@ -78,7 +78,7 @@ describe('parseAndValidateFilterString', () => {
allowedOperatorsByField,
filterFieldSchema: FilterFieldSchema,
}),
- ).toThrowError(`Invalid filter expression format: "nameeqvalue"`);
+ ).toThrowError('Invalid filter expression format: "nameeqvalue"');
expect(() =>
parseAndValidateFilterString({
@@ -86,7 +86,7 @@ describe('parseAndValidateFilterString', () => {
allowedOperatorsByField,
filterFieldSchema: FilterFieldSchema,
}),
- ).toThrowError(`Invalid filter expression format: "name:eq"`);
+ ).toThrowError('Invalid filter expression format: "name:eq"');
});
it('keeps only the last filter for duplicate fields', () => {
diff --git a/src/utils/requiredStrings.ts b/src/utils/requiredStrings.ts
new file mode 100644
index 00000000..8925574f
--- /dev/null
+++ b/src/utils/requiredStrings.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod';
+
+export const requiredString = (property: string): z.ZodString =>
+ z
+ .string({ message: `${property} is required` })
+ .nonempty({ message: `${property} must be non-empty` });
diff --git a/types/process-env.d.ts b/types/process-env.d.ts
index de12eb88..4460b369 100644
--- a/types/process-env.d.ts
+++ b/types/process-env.d.ts
@@ -1,3 +1,6 @@
+// If you add a new environment variable, make sure you update
+// docs/docs/configuration/mcp-config/generator.mdx
+
export interface ProcessEnvEx {
AUTH: string | undefined;
TRANSPORT: string | undefined;
@@ -24,6 +27,15 @@ export interface ProcessEnvEx {
DISABLE_METADATA_API_REQUESTS: string | undefined;
ENABLE_SERVER_LOGGING: string | undefined;
SERVER_LOG_DIRECTORY: string | undefined;
+ DISABLE_OAUTH: string | undefined;
+ OAUTH_ISSUER: string | undefined;
+ OAUTH_JWE_PRIVATE_KEY_PATH: string | undefined;
+ OAUTH_JWE_PRIVATE_KEY: string | undefined;
+ OAUTH_JWE_PRIVATE_KEY_PASSPHRASE: string | undefined;
+ OAUTH_REDIRECT_URI: string | undefined;
+ OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: string | undefined;
+ OAUTH_ACCESS_TOKEN_TIMEOUT_MS: string | undefined;
+ OAUTH_REFRESH_TOKEN_TIMEOUT_MS: string | undefined;
}
declare global {