Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions docs/docs/configuration/mcp-config/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,43 @@ variable.

<hr />

## `TELEMETRY_PROVIDER`

The telemetry provider to use for metrics collection.

- Default: `noop`
- Possible values:
- `noop` - No telemetry (default)
- `custom` - Use a custom telemetry provider (requires
[`TELEMETRY_PROVIDER_CONFIG`](#telemetry_provider_config))

<hr />

## `TELEMETRY_PROVIDER_CONFIG`

Configuration for the custom telemetry provider. Required when
[`TELEMETRY_PROVIDER`](#telemetry_provider) is `custom`.

- Format: JSON string with at least a `module` field
- The `module` field should be a path to a JavaScript file or npm package that exports a class
implementing the `TelemetryProvider` interface.

**Example:**

```bash
TELEMETRY_PROVIDER_CONFIG='{"module": "./my-telemetry-provider.js"}'
```

The custom provider module should export a default class (or named export `TelemetryProvider`) that
implements:

```typescript
interface TelemetryProvider {
initialize(): void;
recordMetric(name: string, value: number, attributes: Record<string, unknown>): void;
}
```

[mcp-transport]: https://modelcontextprotocol.io/docs/concepts/transports
[tab-ds-connections]:
https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm#query_data_source_connections
Expand Down
15 changes: 15 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"build:mcpb": "run-s build:manifest :build:mcpb",
"build:manifest": "tsx src/scripts/createClaudeMcpBundleManifest.ts",
"start:http": "node build/index.js",
"start:http:apm": "node -r ./build/telemetry/tracing.js build/index.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
21 changes: 21 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 { isTelemetryProvider, providerConfigSchema, 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 @@ -78,6 +79,7 @@ export class Config {
clientIdSecretPairs: Record<string, string> | null;
dnsServers: string[];
};
telemetry: TelemetryConfig;

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

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

const parsedProvider = isTelemetryProvider(telemetryProvider) ? telemetryProvider : 'noop';
if (parsedProvider === 'custom') {
if (!telemetryProviderConfig) {
throw new Error(
'TELEMETRY_PROVIDER_CONFIG is required when TELEMETRY_PROVIDER is "custom"',
);
}
this.telemetry = {
provider: 'custom',
providerConfig: providerConfigSchema.parse(JSON.parse(telemetryProviderConfig)),
};
} else {
this.telemetry = {
provider: parsedProvider,
};
}

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

Expand Down
23 changes: 22 additions & 1 deletion src/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */

import { build } from 'esbuild';
import { chmod, rm } from 'fs/promises';
import { chmod, mkdir, rm } from 'fs/promises';

const dev = process.argv.includes('--dev');

Expand Down Expand Up @@ -32,5 +32,26 @@ const dev = process.argv.includes('--dev');
console.log(`⚠️ ${warning.text}`);
}

console.log('🏗️ Building telemetry/tracing.js...');
await mkdir('./build/telemetry', { recursive: true });
const tracingResult = await build({
entryPoints: ['./src/telemetry/tracing.ts'],
bundle: true,
platform: 'node',
format: 'cjs',
minify: !dev,
packages: 'external',
sourcemap: true,
outfile: './build/telemetry/tracing.js',
});

for (const error of tracingResult.errors) {
console.log(`❌ ${error.text}`);
}

for (const warning of tracingResult.warnings) {
console.log(`⚠️ ${warning.text}`);
}

await chmod('./build/index.js', '755');
})();
68 changes: 68 additions & 0 deletions src/telemetry/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { MockInstance } from 'vitest';

import { initializeTelemetry } from './init.js';
import { TelemetryConfig } from './types.js';

const mocks = vi.hoisted(() => ({
mockGetConfig: vi.fn(),
MockNoOpTelemetryProvider: vi.fn(),
}));

vi.mock('../config.js', () => ({
getConfig: mocks.mockGetConfig,
}));

vi.mock('./noop.js', () => ({
NoOpTelemetryProvider: mocks.MockNoOpTelemetryProvider,
}));

describe('initializeTelemetry', () => {
const defaultTelemetryConfig: TelemetryConfig = {
provider: 'noop',
};

let consoleErrorSpy: MockInstance;
let consoleWarnSpy: MockInstance;

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

// Suppress console output
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

// Default mock implementations
mocks.MockNoOpTelemetryProvider.mockImplementation(() => ({
initialize: vi.fn(),
recordMetric: vi.fn(),
}));
});

afterEach(() => {
consoleErrorSpy.mockRestore();
consoleWarnSpy.mockRestore();
});

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

initializeTelemetry();

expect(mocks.MockNoOpTelemetryProvider).toHaveBeenCalled();
});

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

initializeTelemetry();

expect(mocks.MockNoOpTelemetryProvider).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleWarnSpy).toHaveBeenCalledWith('Falling back to NoOp telemetry provider');
});
});
Loading
Loading