Skip to content

Commit b475f20

Browse files
@W-22065947: Allow for disabling the MCP server globally (#309)
* Add BREAK_GLASS_DISABLE_GLOBALLY * Bump * Update MCPB * Update import * Update doc * Bump
1 parent 9781818 commit b475f20

13 files changed

Lines changed: 121 additions & 19 deletions

File tree

docs/docs/configuration/mcp-config/env-vars.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ A comma-separated list of loggers to enable.
5555

5656
- Default: `appLogger`
5757
- Possible values (may be combined): `fileLogger`, `appLogger`
58-
- `fileLogger` — writes log entries and MCP notifications normally only sent to clients to hourly rotating files in the directory specified by[`FILE_LOGGER_DIRECTORY`](#file_logger_directory).
59-
Notifications include tool calls and their arguments as well as HTTP traces for the requests and responses to the Tableau REST APIs.
58+
- `fileLogger` — writes log entries and MCP notifications normally only sent to clients to hourly
59+
rotating files in the directory specified by[`FILE_LOGGER_DIRECTORY`](#file_logger_directory).
60+
Notifications include tool calls and their arguments as well as HTTP traces for the requests and
61+
responses to the Tableau REST APIs.
6062
- `appLogger` — writes log entries to stdout as JSON. Enabled by default when transport is `http`.
6163
- The log file names are in the format `YYYY-MM-DDTHH-00-00-000Z.log` e.g.
6264
`2025-10-15T22-00-00-000Z.log` meaning this log file contains all log messages for hour 22 of
@@ -394,3 +396,25 @@ Enables product telemetry for tool usage tracking.
394396
https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source_connections
395397
[tab-connect-ds]:
396398
https://help.tableau.com/current/api/vizql-data-service/en-us/docs/vds_create_queries.html#connect-to-your-data-source
399+
400+
<hr />
401+
402+
## `BREAK_GLASS_DISABLE_GLOBALLY`
403+
404+
Can be used to force all MCP tools to return a "service unavailable" error message. Use with
405+
discretion.
406+
407+
- Default: `false`
408+
- When `true`, all tools will return the below result:
409+
410+
```json
411+
{
412+
"content": [
413+
{
414+
"type": "text",
415+
"text": "The Tableau MCP server is temporarily unavailable. Please try again later."
416+
}
417+
],
418+
"isError": true
419+
}
420+
```

docs/docs/enterprise/tableau-server.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,7 @@ By default, Tableau MCP sends notifications to MCP clients containing the reques
429429
traces for each request Tableau MCP tools make to the Tableau REST APIs. Many clients will save
430430
these notifications to their own log files, but if you need a way to gather and audit these traces,
431431
server-level logging can be enabled. See
432-
[ENABLED_LOGGERS](../configuration/mcp-config/env-vars#enabled_loggers) for more
433-
information.
432+
[ENABLED_LOGGERS](../configuration/mcp-config/env-vars#enabled_loggers) for more information.
434433

435434
```
436435
ENABLED_LOGGERS=fileLogger
@@ -673,3 +672,9 @@ For more information and precautions, see
673672
```
674673
ENABLE_PASSTHROUGH_AUTH=true
675674
```
675+
676+
### Disable service temporarily
677+
678+
If you need to temporarily disable the service for any reason, you can set
679+
[BREAK_GLASS_DISABLE_GLOBALLY](../configuration/mcp-config/env-vars.md#break_glass_disable_globally)
680+
to `true`. The MCP server will continue to handle requests but all tool calls will return an error.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@tableau/mcp-server",
33
"description": "Helping agents see and understand data.",
4-
"version": "1.18.7",
4+
"version": "1.18.8",
55
"repository": {
66
"type": "git",
77
"url": "git+https://github.com/tableau/tableau-mcp.git"

src/config.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,18 @@ describe('Config', () => {
244244
expect(config.enablePassthroughAuth).toBe(true);
245245
});
246246

247+
it('should set breakGlassDisableGlobally to false by default', () => {
248+
const config = new Config();
249+
expect(config.breakGlassDisableGlobally).toBe(false);
250+
});
251+
252+
it('should set breakGlassDisableGlobally to true when specified', () => {
253+
vi.stubEnv('BREAK_GLASS_DISABLE_GLOBALLY', 'true');
254+
255+
const config = new Config();
256+
expect(config.breakGlassDisableGlobally).toBe(true);
257+
});
258+
247259
describe('HTTP server config parsing', () => {
248260
it('should set sslKey to default when SSL_KEY is not set', () => {
249261
const config = new Config();

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class Config {
8080
productTelemetryEndpoint: string;
8181
productTelemetryEnabled: boolean;
8282
isHyperforce: boolean;
83+
breakGlassDisableGlobally: boolean;
8384

8485
constructor() {
8586
const cleansedVars = removeClaudeMcpBundleUserConfigTemplates(process.env);
@@ -141,6 +142,7 @@ export class Config {
141142
PRODUCT_TELEMETRY_ENDPOINT: productTelemetryEndpoint,
142143
PRODUCT_TELEMETRY_ENABLED: productTelemetryEnabled,
143144
IS_HYPERFORCE: isHyperforce,
145+
BREAK_GLASS_DISABLE_GLOBALLY: breakGlassDisableGlobally,
144146
} = cleansedVars;
145147

146148
let jwtUsername = '';
@@ -268,6 +270,7 @@ export class Config {
268270
productTelemetryEndpoint || 'https://prod.telemetry.tableausoftware.com';
269271
this.productTelemetryEnabled = productTelemetryEnabled !== 'false';
270272
this.isHyperforce = isHyperforce === 'true';
273+
this.breakGlassDisableGlobally = breakGlassDisableGlobally === 'true';
271274

272275
this.auth = isAuthType(auth) ? auth : this.oauth.enabled ? 'oauth' : 'pat';
273276
this.transport = isTransport(transport) ? transport : this.oauth.enabled ? 'http' : 'stdio';

src/errors/mcpToolError.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export class ZodiosValidationError extends McpToolError {
126126
}
127127
}
128128

129+
export class ServiceUnavailableError extends McpToolError {
130+
constructor(message: string) {
131+
super({ type: 'service-unavailable', message, statusCode: 503 });
132+
}
133+
}
134+
129135
export class UnknownError extends McpToolError {
130136
constructor(message: string, statusCode = 500) {
131137
super({ type: 'unknown', message, statusCode });

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ async function startServer(): Promise<void> {
7171
if (config.disableLogMasking) {
7272
writeToStderr('⚠️ Log masking is disabled!');
7373
}
74+
75+
if (config.breakGlassDisableGlobally) {
76+
writeToStderr(
77+
'⚠️ BREAK_GLASS_DISABLE_GLOBALLY is enabled! This means that the MCP server will be disabled globally and will return errors to all users!',
78+
);
79+
}
7480
}
7581

7682
startServer().catch((error) => {

src/scripts/createClaudeMcpBundleManifest.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,14 @@ const envVars = {
570570
required: false,
571571
sensitive: false,
572572
},
573+
BREAK_GLASS_DISABLE_GLOBALLY: {
574+
includeInUserConfig: false,
575+
type: 'boolean',
576+
title: 'Break Glass Disable Globally',
577+
description: 'Force all MCP tools to return a "service unavailable" error message.',
578+
required: false,
579+
sensitive: false,
580+
},
573581
} satisfies EnvVars;
574582

575583
const userConfig = Object.entries(envVars).reduce<Record<string, McpbUserConfigurationOption>>(

src/server.test.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1+
import { ServiceUnavailableError } from './errors/mcpToolError.js';
12
import { exportedForTesting as serverExportedForTesting } from './server.js';
2-
import { testProductVersion } from './testShared.js';
3+
import { stubDefaultEnvVars, testProductVersion } from './testShared.js';
4+
import { exportedForTesting } from './tools/listDatasources/listDatasources.js';
35
import { getQueryDatasourceTool } from './tools/queryDatasource/queryDatasource.js';
6+
import { TableauToolCallback } from './tools/toolContext.js';
7+
import { getMockRequestHandlerExtra } from './tools/toolContext.mock.js';
48
import { toolNames } from './tools/toolName.js';
59
import { toolFactories } from './tools/tools.js';
10+
import invariant from './utils/invariant.js';
611
import { Provider } from './utils/provider.js';
712

813
const { Server } = serverExportedForTesting;
914

1015
describe('server', () => {
11-
const originalEnv = process.env;
12-
1316
beforeEach(() => {
14-
process.env = {
15-
...originalEnv,
16-
INCLUDE_TOOLS: undefined,
17-
EXCLUDE_TOOLS: undefined,
18-
};
17+
vi.unstubAllEnvs();
18+
stubDefaultEnvVars();
1919
});
2020

2121
afterEach(() => {
22-
process.env = { ...originalEnv };
22+
vi.unstubAllEnvs();
2323
});
2424

2525
it('should register tools', async () => {
@@ -63,7 +63,7 @@ describe('server', () => {
6363
});
6464

6565
it('should register tools filtered by includeTools', async () => {
66-
process.env.INCLUDE_TOOLS = 'query-datasource';
66+
vi.stubEnv('INCLUDE_TOOLS', 'query-datasource');
6767
const server = getServer();
6868
await server.registerTools();
6969

@@ -80,7 +80,7 @@ describe('server', () => {
8080
});
8181

8282
it('should register tools filtered by excludeTools', async () => {
83-
process.env.EXCLUDE_TOOLS = 'query-datasource';
83+
vi.stubEnv('EXCLUDE_TOOLS', 'query-datasource');
8484
const server = getServer();
8585
await server.registerTools();
8686

@@ -111,7 +111,7 @@ describe('server', () => {
111111

112112
it('should throw error when no tools are registered', async () => {
113113
const sortedToolNames = [...toolNames].sort((a, b) => a.localeCompare(b)).join(', ');
114-
process.env.EXCLUDE_TOOLS = sortedToolNames;
114+
vi.stubEnv('EXCLUDE_TOOLS', sortedToolNames);
115115
const server = getServer();
116116

117117
const sentences = [
@@ -133,6 +133,32 @@ describe('server', () => {
133133

134134
expect(server.server.setRequestHandler).toHaveBeenCalled();
135135
});
136+
137+
it('should reject tool calls with service unavailable error when BREAK_GLASS_DISABLE_GLOBALLY is true', async () => {
138+
vi.stubEnv('BREAK_GLASS_DISABLE_GLOBALLY', 'true');
139+
140+
const server = getServer();
141+
await server.registerTools();
142+
143+
const listDatasourcesRegistration = vi
144+
.mocked(server.registerTool)
145+
.mock.calls.find((call) => call[0 /* tool name */] === 'list-datasources');
146+
147+
invariant(listDatasourcesRegistration);
148+
const listDatasourcesCallback =
149+
listDatasourcesRegistration[2 /* callback */] as TableauToolCallback<
150+
Partial<typeof exportedForTesting.listDatasourcesParamsSchema>
151+
>;
152+
153+
await expect(listDatasourcesCallback({}, getMockRequestHandlerExtra())).rejects.toSatisfy(
154+
(error: unknown) =>
155+
error instanceof ServiceUnavailableError &&
156+
error.type === 'service-unavailable' &&
157+
error.statusCode === 503 &&
158+
error.message ===
159+
'The Tableau MCP server is temporarily unavailable. Please try again later.',
160+
);
161+
});
136162
});
137163

138164
function getServer(): InstanceType<typeof Server> {

0 commit comments

Comments
 (0)