Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5770f43
telemetry wip
jarhun88 Dec 18, 2025
e672b12
adding missing types.ts
jarhun88 Dec 18, 2025
31d23ae
init telemetry tests
jarhun88 Dec 24, 2025
0e95fb0
fix lint
jarhun88 Jan 5, 2026
3b981b8
remove unnecessary files and made load synchronous
jarhun88 Jan 5, 2026
936736d
loading tracing.js to package
jarhun88 Jan 13, 2026
e4dcea9
remove unused fields
jarhun88 Jan 13, 2026
7bec3f3
bump version
jarhun88 Jan 13, 2026
68b8ca2
make telemetryConfig requirements clear and enforced in runtime
jarhun88 Jan 16, 2026
1bd7295
separate runtime enforcement for non custom provider
jarhun88 Jan 16, 2026
33655a8
spacing
jarhun88 Jan 16, 2026
0091132
introduce custom metrics as counter
jarhun88 Jan 17, 2026
fbc43af
type safety for meter and counter
jarhun88 Jan 17, 2026
482a6c1
type safety for meter and counter
jarhun88 Jan 17, 2026
63b3207
remove dead code and comments
jarhun88 Jan 20, 2026
e8e3c48
remove more comments
jarhun88 Jan 20, 2026
1e3c3d9
remove more comments
jarhun88 Jan 20, 2026
9ce2dd7
add type guard for telemetryProvider
jarhun88 Jan 20, 2026
ff24b7d
removing default in telemetryProvider
jarhun88 Jan 20, 2026
094adbd
check class implements interface
jarhun88 Jan 20, 2026
261e1f2
address nit string calling
jarhun88 Jan 20, 2026
bc372e1
using zod for providerTelemetryConfig
jarhun88 Jan 21, 2026
3fb6c24
updating docs for new env vars
jarhun88 Jan 21, 2026
8207daf
simplify customMetric recording of tools
jarhun88 Jan 21, 2026
bcf048f
removing telemetry_enabled
jarhun88 Jan 21, 2026
26c97b4
Merge branch 'main' into telemetry
jarhun88 Jan 21, 2026
71ee910
refactor init.tests.ts
jarhun88 Jan 21, 2026
1b103e8
lint
jarhun88 Jan 21, 2026
a07e524
added minify and sourcemap and debug logs to build.ts
jarhun88 Jan 21, 2026
cd906e9
remove type assertions and simplified validateTelemetryProvider
jarhun88 Jan 21, 2026
e2f90fd
reduce usage of type assertion
jarhun88 Jan 21, 2026
84141f1
lint
jarhun88 Jan 21, 2026
70bb32c
remove moncloud option
jarhun88 Jan 22, 2026
83ce30a
zoddified all telemetry interfaces
jarhun88 Jan 22, 2026
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
19 changes: 17 additions & 2 deletions package-lock.json

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

8 changes: 5 additions & 3 deletions 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.13.8",
"version": "1.13.9",
"repository": {
"type": "git",
"url": "git+https://github.com/tableau/tableau-mcp.git"
Expand All @@ -22,15 +22,17 @@
"clean": "npx rimraf ./build",
":build": "esbuild src/index.ts --bundle --platform=node --format=cjs --minify --outfile=build/index.js --sourcemap --log-override:empty-import-meta=silent",
":build:dev": "esbuild src/index.ts --bundle --packages=external --platform=node --format=cjs --outfile=build/index.js --sourcemap",
"build": "run-s clean :build exec-perms",
"build:dev": "run-s clean :build:dev exec-perms",
"build": "run-s clean :build :build:tracing exec-perms",
"build:dev": "run-s clean :build:dev :build:tracing exec-perms",
"build:watch": "npm run :build:dev -- --watch",
"build:docker": "docker build -t tableau-mcp .",
":build:mcpb": "npx -y @anthropic-ai/mcpb pack . tableau-mcp.mcpb",
"build:mcpb": "run-s build:manifest:script build:manifest :build:mcpb",
"build:manifest": "node build/scripts/createClaudeMcpBundleManifest.mjs",
"build:manifest:script": "esbuild src/scripts/createClaudeMcpBundleManifest.ts --bundle --platform=node --format=esm --outdir=build/scripts --sourcemap=inline --out-extension:.js=.mjs",
"start:http": "node build/index.js",
"start:http:apm": "node -r ./build/telemetry/tracing.js build/index.js",
":build:tracing": "esbuild src/telemetry/tracing.ts --bundle --packages=external --platform=node --format=cjs --outfile=build/telemetry/tracing.js",
"start:http:docker": "docker run -p 3927:3927 -i --rm --env-file env.list tableau-mcp",
"lint": "npm exec eslint",
"inspect": "npx @modelcontextprotocol/inspector --config config.json --server tableau",
Expand Down
11 changes: 11 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CorsOptions } from 'cors';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import { TelemetryConfig } from './telemetry/types.js';
import { isToolGroupName, isToolName, toolGroups, ToolName } from './tools/toolName.js';
import { isTransport, TransportName } from './transports.js';
import { getDirname } from './utils/getDirname.js';
Expand Down Expand Up @@ -76,6 +77,7 @@ export class Config {
clientIdSecretPairs: Record<string, string> | null;
dnsServers: string[];
};
telemetry: TelemetryConfig;

constructor() {
const cleansedVars = removeClaudeMcpBundleUserConfigTemplates(process.env);
Expand Down Expand Up @@ -129,6 +131,9 @@ export class Config {
OAUTH_AUTHORIZATION_CODE_TIMEOUT_MS: authzCodeTimeoutMs,
OAUTH_ACCESS_TOKEN_TIMEOUT_MS: accessTokenTimeoutMs,
OAUTH_REFRESH_TOKEN_TIMEOUT_MS: refreshTokenTimeoutMs,
TELEMETRY_ENABLED: telemetryEnabled,
TELEMETRY_PROVIDER: telemetryProvider,
TELEMETRY_PROVIDER_CONFIG: telemetryProviderConfig,
} = cleansedVars;

let jwtUsername = '';
Expand Down Expand Up @@ -223,6 +228,12 @@ export class Config {
: null,
};

this.telemetry = {
enabled: telemetryEnabled === 'true',
provider: (telemetryProvider as 'noop' | 'moncloud' | 'custom') || 'noop',
providerConfig: telemetryProviderConfig ? JSON.parse(telemetryProviderConfig) : undefined,
};

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

Expand Down
113 changes: 113 additions & 0 deletions src/telemetry/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';

// Mock the config module
vi.mock('../config.js', () => ({
getConfig: vi.fn(),
}));

// Mock the provider modules
vi.mock('./moncloud.js', () => ({
MonCloudTelemetryProvider: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
addAttributes: vi.fn(),
})),
}));

vi.mock('./noop.js', () => ({
NoOpTelemetryProvider: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
addAttributes: vi.fn(),
})),
}));

import { getConfig } from '../config.js';
import { initializeTelemetry } from './init.js';
import { MonCloudTelemetryProvider } from './moncloud.js';
import { NoOpTelemetryProvider } from './noop.js';
import { TelemetryConfig } from './types.js';

describe('initializeTelemetry', () => {
const mockGetConfig = getConfig as Mock;

const defaultTelemetryConfig: TelemetryConfig = {
enabled: true,
provider: 'noop',
};

beforeEach(() => {
vi.clearAllMocks();
});

// MonCloud tests
it('returns MonCloudTelemetryProvider when provider is "moncloud"', () => {
mockGetConfig.mockReturnValue({
telemetry: { ...defaultTelemetryConfig, provider: 'moncloud' },
});

initializeTelemetry();

expect(MonCloudTelemetryProvider).toHaveBeenCalled();
});

// NoOp tests
it('returns NoOpTelemetryProvider when telemetry is disabled', () => {
mockGetConfig.mockReturnValue({
telemetry: { ...defaultTelemetryConfig, enabled: false },
});

const provider = initializeTelemetry();

expect(NoOpTelemetryProvider).toHaveBeenCalled();
expect(provider.initialize).toBeDefined();
expect(provider.addAttributes).toBeDefined();
});

it('returns NoOpTelemetryProvider when provider is "noop"', () => {
mockGetConfig.mockReturnValue({
telemetry: { ...defaultTelemetryConfig, provider: 'noop' },
});

initializeTelemetry();

expect(NoOpTelemetryProvider).toHaveBeenCalled();
});

it('returns NoOpTelemetryProvider for unknown provider with warning', () => {
mockGetConfig.mockReturnValue({
telemetry: { ...defaultTelemetryConfig, provider: 'unknown-provider' },
});

initializeTelemetry();

expect(NoOpTelemetryProvider).toHaveBeenCalled();
});

it('falls back to NoOpTelemetryProvider on initialization error', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

// Make MonCloudTelemetryProvider throw during initialization
(MonCloudTelemetryProvider as Mock).mockImplementationOnce(() => ({
initialize: vi.fn().mockImplementation(() => {
throw new Error('Init failed');
}),
addAttributes: vi.fn(),
}));

mockGetConfig.mockReturnValue({
telemetry: { ...defaultTelemetryConfig, provider: 'moncloud' },
});

const provider = initializeTelemetry();

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to initialize telemetry provider:',
expect.any(Error),
);
expect(consoleWarnSpy).toHaveBeenCalledWith('Falling back to NoOp telemetry provider');
expect(provider).toBeDefined();

consoleErrorSpy.mockRestore();
consoleWarnSpy.mockRestore();
});
});
156 changes: 156 additions & 0 deletions src/telemetry/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Telemetry initialization and provider factory
*/

import { resolve } from 'path';

import { getConfig } from '../config.js';
import { MonCloudTelemetryProvider } from './moncloud.js';
import { NoOpTelemetryProvider } from './noop.js';
import { TelemetryProvider } from './types.js';

/**
* Initialize the telemetry provider based on configuration.
*
* This function should be called early in application startup, before any
* HTTP requests or other instrumented operations occur.
*
* @returns A configured telemetry provider
*
* @example
* ```typescript
* function main() {
* // Initialize telemetry first
* const telemetry = initializeTelemetry();
*
* // Add global attributes
* telemetry.addAttributes({
* 'tableau.server': config.server,
* 'mcp.version': '1.0.0',
* });
*
* // Start application...
* }
* ```
*/
export function initializeTelemetry(): TelemetryProvider {
const config = getConfig();

// If telemetry is disabled, use NoOp provider
if (!config.telemetry.enabled) {
const provider = new NoOpTelemetryProvider();
provider.initialize();
return provider;
}

let provider: TelemetryProvider;

try {
// Select provider based on configuration
switch (config.telemetry.provider) {
case 'moncloud':
provider = new MonCloudTelemetryProvider();
break;

case 'custom':
// Load custom provider from user's filesystem
provider = loadCustomProvider(config.telemetry.providerConfig);
break;

case 'noop':
default:
if (config.telemetry.provider !== 'noop') {
console.warn(
`Unknown telemetry provider: ${config.telemetry.provider}. Using NoOp provider.`,
);
}
provider = new NoOpTelemetryProvider();
}

// Initialize the provider
provider.initialize();
return provider;
} catch (error) {
console.error('Failed to initialize telemetry provider:', error);
console.warn('Falling back to NoOp telemetry provider');

// Fallback to NoOp on error - telemetry failures shouldn't break the application
const fallbackProvider = new NoOpTelemetryProvider();
fallbackProvider.initialize();
return fallbackProvider;
}
}

/**
* Load a custom telemetry provider from user's filesystem or npm package.
*
* The custom provider module should export a default class that implements TelemetryProvider.
*
* @param config - Provider configuration containing the module path
* @returns A configured custom telemetry provider
*
* @example Custom provider from file
* ```bash
* TELEMETRY_PROVIDER=custom
* TELEMETRY_PROVIDER_CONFIG='{"module":"./my-telemetry.js"}'
* ```
*
* @example Custom provider from npm package
* ```bash
* TELEMETRY_PROVIDER=custom
* TELEMETRY_PROVIDER_CONFIG='{"module":"my-company-telemetry"}'
* ```
*/
function loadCustomProvider(config?: Record<string, unknown>): TelemetryProvider {
if (!config?.module) {
throw new Error(
'Custom telemetry provider requires "module" in providerConfig. ' +
'Example: TELEMETRY_PROVIDER_CONFIG=\'{"module":"./my-telemetry.js"}\'',
);
}

const modulePath = config.module as string;

// Determine if it's a file path or npm package name
let resolvedPath: string;

if (modulePath.startsWith('.') || modulePath.startsWith('/')) {
// File path - resolve relative to process working directory (user's project root)
resolvedPath = resolve(process.cwd(), modulePath);
} else {
// npm package name - require as-is
resolvedPath = modulePath;
}

try {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Sync load for preload script
const module = require(resolvedPath);

// Look for default export or named export "TelemetryProvider"
const ProviderClass = module.default || module.TelemetryProvider;

if (!ProviderClass) {
throw new Error(
`Module ${modulePath} must export a default class or named export "TelemetryProvider" ` +
'that implements the TelemetryProvider interface',
);
}

// Instantiate the provider with the full config
return new ProviderClass(config);
} catch (error) {
// Provide helpful error message with common issues
let errorMessage = `Failed to load custom telemetry provider from "${modulePath}". `;

if ((error as any).code === 'MODULE_NOT_FOUND') {
errorMessage +=
'Module not found. ' +
'If using a file path, ensure the file exists and the path is correct. ' +
'If using an npm package, ensure it is installed.';
} else {
errorMessage += `Error: ${error}`;
}

throw new Error(errorMessage);
}
}
Loading