Skip to content
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
77daa9f
Add constrainSuccessResult
anyoung-tableau Oct 17, 2025
7933d1a
Fix typo
anyoung-tableau Oct 17, 2025
cfbdd9a
Rename
anyoung-tableau Oct 17, 2025
3b7525d
Add isDatasourceAllowed
anyoung-tableau Oct 17, 2025
3b1b43e
Add ResourceAccessChecker
anyoung-tableau Oct 17, 2025
a8940cc
Only cache constant results
anyoung-tableau Oct 20, 2025
e035e0b
Add isViewAllowed
anyoung-tableau Oct 20, 2025
b121f37
Update get-view-image
anyoung-tableau Oct 20, 2025
6bb8fff
Constrain a few Pulse tools
anyoung-tableau Oct 20, 2025
53c4005
Update list-pulse-metric-subscriptions
anyoung-tableau Oct 20, 2025
2317fa3
Require filters to be non-empty
anyoung-tableau Oct 20, 2025
5358b74
Update generate-pulse-metric-value-insight-bundle
anyoung-tableau Oct 20, 2025
f68b608
Constrain search-content
anyoung-tableau Oct 21, 2025
21c9ee7
Add ConstrainedResult type
anyoung-tableau Oct 21, 2025
0d95d2b
Don't cache when project filtering is enabled
anyoung-tableau Oct 21, 2025
3addd0d
Move dotenv to getConfig
anyoung-tableau Oct 21, 2025
2db5593
Add Config tests
anyoung-tableau Oct 21, 2025
b2a8fd3
Introduce ResourceAccessChecker tests
anyoung-tableau Oct 22, 2025
016fc4d
Lazy read config to fix tests
anyoung-tableau Oct 22, 2025
b91d174
Add not allowed tests
anyoung-tableau Oct 22, 2025
9180454
Add coverage for cache hits
anyoung-tableau Oct 22, 2025
29cfefb
Update tool tests
anyoung-tableau Oct 22, 2025
326021a
Add list-datasources tests
anyoung-tableau Oct 22, 2025
30812cb
Add search-content tests
anyoung-tableau Oct 22, 2025
18adabb
Add get-datasource-metadata test
anyoung-tableau Oct 22, 2025
d6d39dc
Fix failing tests
anyoung-tableau Oct 22, 2025
a01434b
Add get-view-* tests
anyoung-tableau Oct 22, 2025
ce1f3da
Add list-views test
anyoung-tableau Oct 22, 2025
b3a6e18
Add get-workbook test
anyoung-tableau Oct 22, 2025
d84cd8b
Add list-workbooks test
anyoung-tableau Oct 22, 2025
156afd6
Add constrainPulseDefinitions tests
anyoung-tableau Oct 23, 2025
1a41ac3
Add constrainPulseMetrics tests
anyoung-tableau Oct 23, 2025
7b47e46
Update Pulse tests to use proper mock data
anyoung-tableau Oct 23, 2025
48a8c53
Add generate-pulse-insight-bundle test
anyoung-tableau Oct 23, 2025
9eb34f9
Add constrainPulseMetricSubscriptions tests
anyoung-tableau Oct 23, 2025
02ed5d2
Sort imports
anyoung-tableau Oct 23, 2025
a24ebee
Add period
anyoung-tableau Oct 23, 2025
65d1043
Fix Batch list metrics URL
anyoung-tableau Oct 23, 2025
766f2d8
String review
anyoung-tableau Oct 23, 2025
7dd24a5
Add docs
anyoung-tableau Oct 23, 2025
b6440c6
More docs
anyoung-tableau Oct 23, 2025
8497864
Bump version
anyoung-tableau Oct 23, 2025
8184009
Update APIs called in docs
anyoung-tableau Oct 24, 2025
f8c06b5
Fix test
anyoung-tableau Oct 24, 2025
a0647e4
Optimize get-workbook
anyoung-tableau Oct 24, 2025
727070d
Fail gracefully when scoping checks fail
anyoung-tableau Oct 24, 2025
22bdbea
Merge remote-tracking branch 'origin/main' into anyoung/bounded-context
anyoung-tableau Oct 24, 2025
7fd8219
Remove extra hr
anyoung-tableau Oct 24, 2025
6f1169b
Fix copy/paste error
anyoung-tableau Oct 24, 2025
9632329
Remove unused link in docs
anyoung-tableau Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions docs/docs/configuration/mcp-config/tool-scoping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
sidebar_position: 6
---

# Tool Scoping

The Tableau MCP server can be configured to limit the scope of its tools to a set of data sources,
workbooks, or projects.

Enabling tool scoping can cause:

1. Tools to return an error if they are called with arguments that are not within the allowed scope,
and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this "and" intentional or did you mean to add more to this sentence?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intentional but I can remove it. The 2nd bullet is the continuation of the sentence.

2. Tools to respond with results that have been filtered to only include content from the allowed
scope.

## Examples use-cases

- Only allow clients to query a single data source with the
[Query Data Source](../../tools/data-qna/query-datasource.md) tool. A client attempting to query
any other data source will result in an error.
- Filter the results of the [List Workbooks](../../tools/workbooks/list-workbooks.md) tool to only
include workbooks that exist in a single project. Workbooks from other projects will not be
included in the results.

## Environment variables

The following optional environment variables can be used to configure the tool scoping.

### `INCLUDE_PROJECT_IDS`

A comma-separated list of project IDs by which to constrain tool arguments and results. Only data
sources and workbooks (or views from those workbooks) that are members of the provided projects can
be queried or will be included in the results of the tools.

- When set, cannot be empty.
- Project IDs can be determined using the
[Query Projects](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_projects.htm#query_projects)
REST API or by the [List Data Sources](../../tools/data-qna/list-datasources.md),
[List Workbooks](../../tools/workbooks/list-workbooks.md), and
[List Views](../../tools/views/list-views.md) tools (assuming tool scoping is disabled).
- Has no impact on the results of the Pulse-related tools.

Example: `d87d843b-4326-4ce3-bc50-a68c1e6c9ca5`

:::warning

To constrain the results of the [Search Content](../../tools/content-exploration/search-content.md)
tool by project, you must also provide the project ID found in the project's URL in the Explore
section of your Tableau site e.g. `861566` from
`https://10ax.online.tableau.com/#/site/my-site/projects/861566`

Example: `d87d843b-4326-4ce3-bc50-a68c1e6c9ca5,861566`

:::

<hr />

### `INCLUDE_DATASOURCE_IDS`

A comma-separated list of data source IDs by which to constrain tool arguments and results. Only
data sources or Pulse metrics and definitions derived from those data sources can be queried or will
be included in the results of the tools.

- When set, cannot be empty.
- Data source IDs can be determined using the
[Query Data Sources](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_sources)
REST API or the [List Data Sources](../../tools/data-qna/list-datasources.md) tool (assuming tool
scoping is disabled).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps note that limiting a datasource does not limit workbooks or views (assuming that's how that works)


Example: `2d935df8-fe7e-4fd8-bb14-35eb4ba31d4`

<hr />

### `INCLUDE_WORKBOOK_IDS`

A comma-separated list of workbook IDs by which to constrain tool arguments and results. Only
workbooks or views from those workbooks can be queried or will be included in the results of the
tools.

- When set, cannot be empty.
- Workbook IDs can be determined using the
[Query Workbooks](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#query_workbooks)
REST API or the [List Workbooks](../../tools/workbooks/list-workbooks.md) tool (assuming tool
scoping is disabled). The [List Views](../../tools/views/list-views.md) tools also return workbook
IDs.
- Has no impact on the results of the Pulse-related tools.

Example: `222ea993-9391-4910-a167-56b3d19b4e3b`

<hr />

<hr />

[cors]: https://expressjs.com/en/resources/middleware/cors.html#configuration-options
2 changes: 2 additions & 0 deletions docs/docs/tools/data-qna/get-datasource-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Fetches field metadata for the specified datasource.

- [Request data source metadata](https://help.tableau.com/current/api/vizql-data-service/en-us/reference/index.html#tag/HeadlessBI/operation/ReadMetadata)
- [Metadata API](https://help.tableau.com/current/api/metadata_api/en-us/index.html)
- [Query Data Source](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source)
(if data source [tool scoping](../../configuration/mcp-config/tool-scoping.md) is enabled)

## Environment variables

Expand Down
4 changes: 4 additions & 0 deletions docs/docs/tools/data-qna/query-datasource.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ data.
## APIs called

- [Query Data Source](https://help.tableau.com/current/api/vizql-data-service/en-us/reference/index.html#tag/HeadlessBI/operation/QueryDatasource)
from VizQL Data Service
- [Query Data Source](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source)
from REST API (if data source [tool scoping](../../configuration/mcp-config/tool-scoping.md) is
enabled)

## Environment variables

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/tools/pulse/list-pulse-metric-subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Retrieves a list of published Pulse Metric Subscriptions for the current user.
## APIs called

- [List subscriptions](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_pulse.htm#PulseSubscriptionService_ListSubscriptions)
- [Batch list metrics](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_pulse.htm#MetricQueryService_BatchGetMetricsByPost)
(if data source [tool scoping](../../configuration/mcp-config/tool-scoping.md) is enabled)

## Example result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Retrieves a list of published Pulse metrics from a list of metric IDs.

## APIs called

- [Batch list metrics](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_pulse.htm#MetricQueryService_BatchGetMetrics)
- [Batch list metrics](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_pulse.htm#MetricQueryService_BatchGetMetricsByPost)

## Required arguments

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/tools/views/get-view-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Retrieves data in comma separated value (CSV) format for the specified view in a
## APIs called

- [Query View Data](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#query_view_data)
- [Get View](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#get_view)
(if workbook or project [tool scoping](../../configuration/mcp-config/tool-scoping.md) is enabled)

## Required arguments

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/tools/views/get-view-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Retrieves an image of the specified view in a Tableau workbook.
## APIs called

- [Query View Image](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#query_view_image)
- [Get View](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#get_view)
(if workbook or project [tool scoping](../../configuration/mcp-config/tool-scoping.md) is enabled)

## Required arguments

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tableau/mcp-server",
"description": "An MCP server for Tableau, providing a suite of tools that will make it easier for developers to build AI applications that integrate with Tableau.",
"version": "1.10.2",
"version": "1.11.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tableau/tableau-mcp.git"
Expand Down
74 changes: 74 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ describe('Config', () => {
MAX_RESULT_LIMIT: undefined,
DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION: undefined,
DISABLE_METADATA_API_REQUESTS: undefined,
ENABLE_SERVER_LOGGING: undefined,
SERVER_LOG_DIRECTORY: undefined,
INCLUDE_PROJECT_IDS: undefined,
INCLUDE_DATASOURCE_IDS: undefined,
INCLUDE_WORKBOOK_IDS: undefined,
};
});

Expand Down Expand Up @@ -647,4 +652,73 @@ describe('Config', () => {
expect(config.jwtAdditionalPayload).toBe('{}');
});
});

describe('Bounded context parsing', () => {
it('should set boundedContext to null sets when no project, datasource, or workbook IDs are provided', () => {
process.env = {
...process.env,
...defaultEnvVars,
};

const config = new Config();
expect(config.boundedContext).toEqual({
projectIds: null,
datasourceIds: null,
workbookIds: null,
});
});

it('should set boundedContext to the specified project, datasource, and workbook IDs when provided', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_PROJECT_IDS: ' 123, 456, 123 ', // spacing is intentional here to test trimming
INCLUDE_DATASOURCE_IDS: '789,101',
INCLUDE_WORKBOOK_IDS: '112,113',
};

const config = new Config();
expect(config.boundedContext).toEqual({
projectIds: new Set(['123', '456']),
datasourceIds: new Set(['789', '101']),
workbookIds: new Set(['112', '113']),
});
});

it('should throw error when INCLUDE_PROJECT_IDS is set to an empty string', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_PROJECT_IDS: '',
};

expect(() => new Config()).toThrow(
'When set, the environment variable INCLUDE_PROJECT_IDS must have at least one value',
);
});

it('should throw error when INCLUDE_DATASOURCE_IDS is set to an empty string', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_DATASOURCE_IDS: '',
};

expect(() => new Config()).toThrow(
'When set, the environment variable INCLUDE_DATASOURCE_IDS must have at least one value',
);
});

it('should throw error when INCLUDE_WORKBOOK_IDS is set to an empty string', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_WORKBOOK_IDS: '',
};

expect(() => new Config()).toThrow(
'When set, the environment variable INCLUDE_WORKBOOK_IDS must have at least one value',
);
});
});
});
49 changes: 49 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
const authTypes = ['pat', 'direct-trust'] as const;
type AuthType = (typeof authTypes)[number];

export type BoundedContext = {
projectIds: Set<string> | null;
datasourceIds: Set<string> | null;
workbookIds: Set<string> | null;
};

export class Config {
auth: AuthType;
server: string;
Expand All @@ -37,6 +43,7 @@ export class Config {
disableMetadataApiRequests: boolean;
enableServerLogging: boolean;
serverLogDirectory: string;
boundedContext: BoundedContext;

constructor() {
const cleansedVars = removeClaudeMcpBundleUserConfigTemplates(process.env);
Expand Down Expand Up @@ -66,6 +73,9 @@ export class Config {
DISABLE_METADATA_API_REQUESTS: disableMetadataApiRequests,
ENABLE_SERVER_LOGGING: enableServerLogging,
SERVER_LOG_DIRECTORY: serverLogDirectory,
INCLUDE_PROJECT_IDS: includeProjectIds,
INCLUDE_DATASOURCE_IDS: includeDatasourceIds,
INCLUDE_WORKBOOK_IDS: includeWorkbookIds,
} = cleansedVars;

const defaultPort = 3927;
Expand All @@ -86,6 +96,29 @@ export class Config {
this.disableMetadataApiRequests = disableMetadataApiRequests === 'true';
this.enableServerLogging = enableServerLogging === 'true';
this.serverLogDirectory = serverLogDirectory || join(__dirname, 'logs');
this.boundedContext = {
projectIds: createSetFromCommaSeparatedString(includeProjectIds),
datasourceIds: createSetFromCommaSeparatedString(includeDatasourceIds),
workbookIds: createSetFromCommaSeparatedString(includeWorkbookIds),
};

if (this.boundedContext.projectIds?.size === 0) {
throw new Error(
'When set, the environment variable INCLUDE_PROJECT_IDS must have at least one value',
);
}

if (this.boundedContext.datasourceIds?.size === 0) {
throw new Error(
'When set, the environment variable INCLUDE_DATASOURCE_IDS must have at least one value',
);
}

if (this.boundedContext.workbookIds?.size === 0) {
throw new Error(
'When set, the environment variable INCLUDE_WORKBOOK_IDS must have at least one value',
);
}

const maxResultLimitNumber = maxResultLimit ? parseInt(maxResultLimit) : NaN;
this.maxResultLimit =
Expand Down Expand Up @@ -181,6 +214,22 @@ function getCorsOriginConfig(corsOriginConfig: string): CorsOptions['origin'] {
}
}

// Creates a set from a comma-separated string of values.
// Returns null if the value is undefined.
function createSetFromCommaSeparatedString(value: string | undefined): Set<string> | null {
if (value === undefined) {
return null;
}

return new Set(
Copy link
Contributor

@stephendeoca stephendeoca Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perform validation of these strings?

Like check that the string is either all numbers or follows the LUID format?

I'm not sure if there's an exact format for LUIDs, but let's assume that there is one. If we had a set of rules such as 32 characters equally separated by 4 dashes. Then we can have an error message in case some one configuring the MCP server accidentally copy pastes 31 characters and then they know why their tools aren't letting them query a datasource or something.

value
.trim()
.split(',')
.map((id) => id.trim())
.filter(Boolean),
);
}

// When the user does not provide a site name in the Claude MCP Bundle configuration,
// Claude doesn't replace its value and sets the site name to "${user_config.site_name}".
function removeClaudeMcpBundleUserConfigTemplates(
Expand Down
24 changes: 24 additions & 0 deletions src/scripts/createClaudeMcpBundleManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,30 @@ const envVars = {
required: false,
sensitive: false,
},
INCLUDE_PROJECT_IDS: {
includeInUserConfig: false,
type: 'string',
title: 'IDs of projects to constrain tool results by',
description: 'A comma-separated list of project IDs to constrain tool results by.',
required: false,
sensitive: false,
},
INCLUDE_DATASOURCE_IDS: {
includeInUserConfig: false,
type: 'string',
title: 'IDs of datasources to constrain tool results by',
description: 'A comma-separated list of datasource IDs to constrain tool results by.',
required: false,
sensitive: false,
},
INCLUDE_WORKBOOK_IDS: {
includeInUserConfig: false,
type: 'string',
title: 'IDs of workbooks to constrain tool results by',
description: 'A comma-separated list of workbook IDs to constrain tool results by.',
required: false,
sensitive: false,
},
MAX_RESULT_LIMIT: {
includeInUserConfig: false,
type: 'number',
Expand Down
Loading