Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,17 @@ These config files will be used in tool configuration explained below.

#### Optional Environment Variables

| **Variable** | **Description** | **Default** | **Note** |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRANSPORT` | The MCP transport type to use for the server. | `stdio` | Possible values are `stdio` or `http`. For `http`, see [HTTP Server Configuration](#http-server-configuration) below for additional variables. See [Transports][mcp-transport] for details. |
| `AUTH` | The authentication method to use by the server. | `pat` | Possible values are `pat` or `direct-trust`. See below sections for additional required variables depending on the desired method. |
| `DEFAULT_LOG_LEVEL` | The default logging level of the server. | `debug` | |
| `DATASOURCE_CREDENTIALS` | A JSON string that includes usernames and passwords for any datasources that require them. | Empty string | Format is provided in the [DATASOURCE_CREDENTIALS](#datasource_credentials) section below. |
| `DISABLE_LOG_MASKING` | Disable masking of credentials in logs. For debug purposes only. | `false` | |
| `INCLUDE_TOOLS` | A comma-separated list of tool names to include in the server. Only these tools will be available. | Empty string (_all_ are included) | For a list of available tools, see [toolName.ts](src/tools/toolName.ts). |
| `EXCLUDE_TOOLS` | A comma-separated list of tool names to exclude from the server. All other tools will be available. | Empty string (_none_ are excluded) | Cannot be provided with `INCLUDE_TOOLS`. |
| `MAX_RESULT_LIMIT` | If a tool has a "limit" parameter and returns an array of items, the maximum length of that array. | Empty string (_no limit_) | A positive number. |
| `DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION` | Disable validation of SET and MATCH filter values in query-datasource tool. | `false` | When `true`, skips validation that checks if filter values exist in the target field. |
| **Variable** | **Description** | **Default** | **Note** |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRANSPORT` | The MCP transport type to use for the server. | `stdio` | Possible values are `stdio` or `http`. For `http`, see [HTTP Server Configuration](#http-server-configuration) below for additional variables. See [Transports][mcp-transport] for details. |
| `AUTH` | The authentication method to use by the server. | `pat` | Possible values are `pat` or `direct-trust`. See below sections for additional required variables depending on the desired method. |
| `DEFAULT_LOG_LEVEL` | The default logging level of the server. | `debug` | |
| `DATASOURCE_CREDENTIALS` | A JSON string that includes usernames and passwords for any datasources that require them. | Empty string | Format is provided in the [DATASOURCE_CREDENTIALS](#datasource_credentials) section below. |
| `DISABLE_LOG_MASKING` | Disable masking of credentials in logs. For debug purposes only. | `false` | |
| `INCLUDE_TOOLS` | A comma-separated list of tool or tool group names to include in the server. Only these tools will be available. | Empty string (_all_ are included) | For a list of available tools and groups, see [toolName.ts](src/tools/toolName.ts). Mixing tool names and group names is allowed. |
| `EXCLUDE_TOOLS` | A comma-separated list of tool or tool group names to exclude from the server. All other tools will be available. | Empty string (_none_ are excluded) | Cannot be provided with `INCLUDE_TOOLS`. |
| `MAX_RESULT_LIMIT` | If a tool has a "limit" parameter and returns an array of items, the maximum length of that array. | Empty string (_no limit_) | A positive number. |
| `DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION` | Disable validation of SET and MATCH filter values in query-datasource tool. | `false` | When `true`, skips validation that checks if filter values exist in the target field. |

#### HTTP Server Configuration

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",
"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.6.0",
"version": "1.7.0",
"homepage": "https://github.com/tableau/tableau-mcp",
"bugs": "https://github.com/tableau/tableau-mcp/issues",
"author": "Tableau",
Expand Down
34 changes: 33 additions & 1 deletion src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ describe('Config', () => {
expect(config.includeTools).toEqual(['query-datasource', 'list-fields']);
});

it('should parse INCLUDE_TOOLS into an array of valid tool names when tool group names are used', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_TOOLS: 'query-datasource,workbook',
};

const config = new Config();
expect(config.includeTools).toEqual(['query-datasource', 'list-workbooks', 'get-workbook']);
});

it('should parse EXCLUDE_TOOLS into an array of valid tool names', () => {
process.env = {
...process.env,
Expand All @@ -288,6 +299,17 @@ describe('Config', () => {
expect(config.excludeTools).toEqual(['query-datasource']);
});

it('should parse EXCLUDE_TOOLS into an array of valid tool names when tool group names are used', () => {
process.env = {
...process.env,
...defaultEnvVars,
EXCLUDE_TOOLS: 'query-datasource,workbook',
};

const config = new Config();
expect(config.excludeTools).toEqual(['query-datasource', 'list-workbooks', 'get-workbook']);
});

it('should filter out invalid tool names from INCLUDE_TOOLS', () => {
process.env = {
...process.env,
Expand Down Expand Up @@ -318,7 +340,17 @@ describe('Config', () => {
EXCLUDE_TOOLS: 'list-fields',
};

expect(() => new Config()).toThrow('Cannot specify both INCLUDE_TOOLS and EXCLUDE_TOOLS');
expect(() => new Config()).toThrow('Cannot include and exclude tools simultaneously');
});

it('should throw error when both INCLUDE_TOOLS and EXCLUDE_TOOLS are specified with tool group names', () => {
process.env = {
...process.env,
...defaultEnvVars,
INCLUDE_TOOLS: 'datasource',
EXCLUDE_TOOLS: 'workbook',
};
expect(() => new Config()).toThrow('Cannot include and exclude tools simultaneously');
});
});

Expand Down
63 changes: 48 additions & 15 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { CorsOptions } from 'cors';

import { isToolName, ToolName } from './tools/toolName.js';
import {
isToolGroupName,
isToolName,
isToolRegistrationMode,
toolGroups,
ToolName,
ToolRegistrationMode,
} from './tools/toolName.js';
import { isTransport, TransportName } from './transports.js';
import invariant from './utils/invariant.js';

Expand Down Expand Up @@ -28,6 +35,7 @@ export class Config {
disableLogMasking: boolean;
includeTools: Array<ToolName>;
excludeTools: Array<ToolName>;
toolRegistrationMode: ToolRegistrationMode;
maxResultLimit: number | null;
disableQueryDatasourceFilterValidation: boolean;

Expand All @@ -54,6 +62,7 @@ export class Config {
DISABLE_LOG_MASKING: disableLogMasking,
INCLUDE_TOOLS: includeTools,
EXCLUDE_TOOLS: excludeTools,
TOOL_REGISTRATION_MODE: toolRegistrationMode,
MAX_RESULT_LIMIT: maxResultLimit,
DISABLE_QUERY_DATASOURCE_FILTER_VALIDATION: disableQueryDatasourceFilterValidation,
} = cleansedVars;
Expand All @@ -73,27 +82,51 @@ export class Config {
this.defaultLogLevel = defaultLogLevel ?? 'debug';
this.disableLogMasking = disableLogMasking === 'true';
this.disableQueryDatasourceFilterValidation = disableQueryDatasourceFilterValidation === 'true';
this.toolRegistrationMode = isToolRegistrationMode(toolRegistrationMode)
? toolRegistrationMode
: 'auto';

const maxResultLimitNumber = maxResultLimit ? parseInt(maxResultLimit) : NaN;
this.maxResultLimit =
isNaN(maxResultLimitNumber) || maxResultLimitNumber <= 0 ? null : maxResultLimitNumber;

this.includeTools = includeTools
? includeTools
.split(',')
.map((s) => s.trim())
.filter(isToolName)
: [];

this.excludeTools = excludeTools
? excludeTools
.split(',')
.map((s) => s.trim())
.filter(isToolName)
: [];
if (this.toolRegistrationMode === 'task') {
this.includeTools = ['start-task'];
this.excludeTools = [];

if (includeTools) {
throw new Error(
'The environment variable INCLUDE_TOOLS cannot be set when tool registration mode is "task"',
);
}

if (excludeTools) {
throw new Error(
'The environment variable EXCLUDE_TOOLS cannot be set when tool registration mode is "task"',
);
}
} else {
this.includeTools = includeTools
? includeTools.split(',').flatMap((s) => {
const v = s.trim();
return isToolName(v) && v !== 'start-task' && v !== 'complete-task'
? v
: isToolGroupName(v)
? toolGroups[v]
: [];
})
: [];

this.excludeTools = excludeTools
? excludeTools.split(',').flatMap((s) => {
const v = s.trim();
return isToolName(v) ? v : isToolGroupName(v) ? toolGroups[v] : [];
})
: [];
}

if (this.includeTools.length > 0 && this.excludeTools.length > 0) {
throw new Error('Cannot specify both INCLUDE_TOOLS and EXCLUDE_TOOLS');
throw new Error('Cannot include and exclude tools simultaneously');
}

invariant(server, 'The environment variable SERVER is not set');
Expand Down
8 changes: 8 additions & 0 deletions src/scripts/createClaudeDesktopExtensionManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ const envVars = {
required: false,
sensitive: false,
},
TOOL_REGISTRATION_MODE: {
includeInUserConfig: false,
type: 'string',
title: 'Tool Registration Mode',
description: 'Set to "task" to enable task mode.',
required: false,
sensitive: false,
},
MAX_RESULT_LIMIT: {
includeInUserConfig: false,
type: 'number',
Expand Down
4 changes: 4 additions & 0 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ describe('server', () => {

const tools = toolFactories.map((tool) => tool(server));
for (const tool of tools) {
if (tool.name === 'complete-task' || tool.name === 'start-task') {
continue;
}

expect(server.tool).toHaveBeenCalledWith(
tool.name,
tool.description,
Expand Down
40 changes: 32 additions & 8 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';

import pkg from '../package.json' with { type: 'json' };
import { getConfig } from './config.js';
import { setLogLevel } from './logging/log.js';
import { Tool } from './tools/tool.js';
import { toolNames } from './tools/toolName.js';
import { ToolName, toolNames } from './tools/toolName.js';
import { toolFactories } from './tools/tools.js';

export const serverName = pkg.name;
Expand All @@ -14,6 +14,7 @@ export const serverVersion = pkg.version;
export class Server extends McpServer {
readonly name: string;
readonly version: string;
readonly registeredTools: Map<ToolName, RegisteredTool> = new Map();

constructor() {
super(
Expand All @@ -24,7 +25,9 @@ export class Server extends McpServer {
{
capabilities: {
logging: {},
tools: {},
tools: {
listChanged: getConfig().toolRegistrationMode === 'task',
},
},
},
);
Expand All @@ -33,15 +36,24 @@ export class Server extends McpServer {
this.version = serverVersion;
}

registerTools = (): void => {
registerTools = (
overrides?: Partial<{
includeTools: Array<ToolName>;
excludeTools: Array<ToolName>;
}>,
): void => {
this.registeredTools.forEach((tool) => tool.remove());
this.registeredTools.clear();

for (const {
name,
description,
paramsSchema,
annotations,
callback,
} of this._getToolsToRegister()) {
this.tool(name, description, paramsSchema, annotations, callback);
} of this._getToolsToRegister(overrides)) {
const tool = this.tool(name, description, paramsSchema, annotations, callback);
this.registeredTools.set(name, tool);
}
};

Expand All @@ -52,8 +64,16 @@ export class Server extends McpServer {
});
};

private _getToolsToRegister = (): Array<Tool<any>> => {
const { includeTools, excludeTools } = getConfig();
private _getToolsToRegister = (
overrides?: Partial<{
includeTools: Array<ToolName>;
excludeTools: Array<ToolName>;
}>,
): Array<Tool<any>> => {
const config = getConfig();
let { includeTools, excludeTools } = overrides ?? config;
includeTools = includeTools ?? config.includeTools;
excludeTools = excludeTools ?? config.excludeTools;

const tools = toolFactories.map((tool) => tool(this));
const toolsToRegister = tools.filter((tool) => {
Expand All @@ -65,6 +85,10 @@ export class Server extends McpServer {
return !excludeTools.includes(tool.name);
}

if (tool.name === 'start-task' || tool.name === 'complete-task') {
return false;
}

return true;
});

Expand Down
36 changes: 36 additions & 0 deletions src/tools/completeTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { Ok } from 'ts-results-es';

import { Server } from '../server.js';
import { Tool } from './tool.js';

const paramsSchema = {};

export const getCompleteTaskTool = (server: Server): Tool<typeof paramsSchema> => {
const completeTaskTool = new Tool({
server,
name: 'complete-task',
description: `Completes a task.`,
paramsSchema,
annotations: {
title: 'Complete Task',
readOnlyHint: true,
openWorldHint: false,
},
callback: async (_, { requestId }): Promise<CallToolResult> => {
return await completeTaskTool.logAndExecute({
requestId,
args: {},
callback: async () => {
server.registerTools({
includeTools: ['start-task'],
});

return new Ok('success');
},
});
},
});

return completeTaskTool;
};
Loading