Skip to content

chore: add type check for CI #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 29, 2025
Merged
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/index.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"preLaunchTask": "tsc: build - tsconfig.build.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineConfig([
files,
languageOptions: {
parserOptions: {
project: "./tsconfig.lint.json",
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
},
Expand Down
6 changes: 1 addition & 5 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
"scripts": {
"prepare": "npm run build",
"build:clean": "rm -rf dist",
"build:compile": "tsc",
"build:compile": "tsc --project tsconfig.build.json",
"build:chmod": "chmod +x dist/index.js",
"build": "npm run build:clean && npm run build:compile && npm run build:chmod",
"inspect": "npm run build && mcp-inspector -- dist/index.js",
"prettier": "prettier",
"check": "npm run build && npm run check:lint && npm run check:format",
"check": "npm run build && npm run check:types && npm run check:lint && npm run check:format",
"check:lint": "eslint .",
"check:format": "prettier -c .",
"check:types": "tsc --noEmit --project tsconfig.json",
"reformat": "prettier --write .",
"generate": "./scripts/generate.sh",
"test": "jest --coverage"
Expand Down
1 change: 1 addition & 0 deletions scripts/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ async function main() {
const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document;
for (const path in openapi.paths) {
for (const method in openapi.paths[path]) {
// @ts-expect-error This is a workaround for the OpenAPI types
const operation = openapi.paths[path][method] as OpenAPIV3_1.OperationObject;

if (!operation.operationId || !operation.tags?.length) {
Expand Down
3 changes: 3 additions & 0 deletions scripts/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
for (const path in openapi.paths) {
const filteredMethods = {} as OpenAPIV3_1.PathItemObject;
for (const method in openapi.paths[path]) {
// @ts-expect-error This is a workaround for the OpenAPI types
if (allowedOperations.includes((openapi.paths[path][method] as { operationId: string }).operationId)) {
// @ts-expect-error This is a workaround for the OpenAPI types
filteredMethods[method] = openapi.paths[path][method] as OpenAPIV3_1.OperationObject;
}
}
if (Object.keys(filteredMethods).length > 0) {
// @ts-expect-error This is a workaround for the OpenAPI types
filteredPaths[path] = filteredMethods;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FetchOptions } from "openapi-fetch";
import { AccessToken, ClientCredentials } from "simple-oauth2";
import { ApiClientError } from "./apiClientError.js";
import { paths, operations } from "./openapi.js";
import { BaseEvent } from "../../telemetry/types.js";
import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
import { packageInfo } from "../../packageInfo.js";

const ATLAS_API_VERSION = "2025-03-12";
Expand Down Expand Up @@ -123,7 +123,7 @@ export class ApiClient {
}>;
}

async sendEvents(events: BaseEvent[]): Promise<void> {
async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
let endpoint = "api/private/unauth/telemetry/events";
const headers: Record<string, string> = {
Accept: "application/json",
Expand Down
3 changes: 1 addition & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export class Server {
timestamp: new Date().toISOString(),
source: "mdbmcp",
properties: {
...this.telemetry.getCommonProperties(),
result: "success",
duration_ms: commandDuration,
component: "server",
Expand All @@ -119,7 +118,7 @@ export class Server {
if (command === "start") {
event.properties.startup_time_ms = commandDuration;
event.properties.read_only_mode = this.userConfig.readOnly || false;
event.properties.disallowed_tools = this.userConfig.disabledTools || [];
event.properties.disabled_tools = this.userConfig.disabledTools || [];
}
if (command === "stop") {
event.properties.runtime_duration_ms = Date.now() - this.startTime;
Expand Down
2 changes: 1 addition & 1 deletion src/telemetry/eventCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class EventCache {
private cache: LRUCache<number, BaseEvent>;
private nextId = 0;

private constructor() {
constructor() {
this.cache = new LRUCache({
max: EventCache.MAX_EVENTS,
// Using FIFO eviction strategy for events
Expand Down
7 changes: 6 additions & 1 deletion src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ export class Telemetry {
*/
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
try {
await client.sendEvents(events);
await client.sendEvents(
events.map((event) => ({
...event,
properties: { ...this.getCommonProperties(), ...event.properties },
}))
);
return { success: true };
} catch (error) {
return {
Expand Down
56 changes: 27 additions & 29 deletions src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,46 @@ export type TelemetryBoolSet = "true" | "false";
/**
* Base interface for all events
*/
export interface Event {
export type TelemetryEvent<T> = {
timestamp: string;
source: "mdbmcp";
properties: Record<string, unknown>;
}

export interface BaseEvent extends Event {
properties: CommonProperties & {
properties: T & {
component: string;
duration_ms: number;
result: TelemetryResult;
category: string;
} & Event["properties"];
}
};
};

export type BaseEvent = TelemetryEvent<unknown>;

/**
* Interface for tool events
*/
export interface ToolEvent extends BaseEvent {
properties: {
command: string;
error_code?: string;
error_type?: string;
project_id?: string;
org_id?: string;
cluster_name?: string;
is_atlas?: boolean;
} & BaseEvent["properties"];
}
export type ToolEventProperties = {
command: string;
error_code?: string;
error_type?: string;
project_id?: string;
org_id?: string;
cluster_name?: string;
is_atlas?: boolean;
};

export type ToolEvent = TelemetryEvent<ToolEventProperties>;
/**
* Interface for server events
*/
export interface ServerEvent extends BaseEvent {
properties: {
command: ServerCommand;
reason?: string;
startup_time_ms?: number;
runtime_duration_ms?: number;
read_only_mode?: boolean;
disabled_tools?: string[];
} & BaseEvent["properties"];
}
export type ServerEventProperties = {
command: ServerCommand;
reason?: string;
startup_time_ms?: number;
runtime_duration_ms?: number;
read_only_mode?: boolean;
disabled_tools?: string[];
};

export type ServerEvent = TelemetryEvent<ServerEventProperties>;

/**
* Interface for static properties, they can be fetched once and reused.
Expand All @@ -69,6 +66,7 @@ export type CommonStaticProperties = {
* Common properties for all events that might change.
*/
export type CommonProperties = {
device_id?: string;
mcp_client_version?: string;
mcp_client_name?: string;
config_atlas_auth?: TelemetryBoolSet;
Expand Down
1 change: 0 additions & 1 deletion src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export abstract class ToolBase {
timestamp: new Date().toISOString(),
source: "mdbmcp",
properties: {
...this.telemetry.getCommonProperties(),
command: this.name,
category: this.category,
component: "tool",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/inMemoryTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";

export class InMemoryTransport implements Transport {
private outputController: ReadableStreamDefaultController<JSONRPCMessage>;
private outputController: ReadableStreamDefaultController<JSONRPCMessage> | undefined;

private startPromise: Promise<unknown>;

Expand Down Expand Up @@ -35,13 +35,13 @@ export class InMemoryTransport implements Transport {
}

send(message: JSONRPCMessage): Promise<void> {
this.outputController.enqueue(message);
this.outputController?.enqueue(message);
return Promise.resolve();
}

// eslint-disable-next-line @typescript-eslint/require-await
async close(): Promise<void> {
this.outputController.close();
this.outputController?.close();
this.onclose?.();
}
onclose?: (() => void) | undefined;
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function parseTable(text: string): Record<string, string>[] {
return data
.filter((_, index) => index >= 2)
.map((cells) => {
const row = {};
const row: Record<string, string> = {};
cells.forEach((cell, index) => {
row[headers[index]] = cell;
});
Expand Down
31 changes: 26 additions & 5 deletions tests/unit/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ describe("Telemetry", () => {

// Helper function to create properly typed test events
function createTestEvent(options?: {
source?: string;
result?: TelemetryResult;
component?: string;
category?: string;
command?: string;
duration_ms?: number;
}): BaseEvent {
}): Omit<BaseEvent, "properties"> & {
properties: {
component: string;
duration_ms: number;
result: TelemetryResult;
category: string;
command: string;
};
} {
return {
timestamp: new Date().toISOString(),
source: options?.source || "mdbmcp",
source: "mdbmcp",
properties: {
component: options?.component || "test-component",
duration_ms: options?.duration_ms || 100,
Expand All @@ -48,6 +55,12 @@ describe("Telemetry", () => {
appendEventsCalls = 0,
sendEventsCalledWith = undefined,
appendEventsCalledWith = undefined,
}: {
sendEventsCalls?: number;
clearEventsCalls?: number;
appendEventsCalls?: number;
sendEventsCalledWith?: BaseEvent[] | undefined;
appendEventsCalledWith?: BaseEvent[] | undefined;
} = {}) {
const { calls: sendEvents } = mockApiClient.sendEvents.mock;
const { calls: clearEvents } = mockEventCache.clearEvents.mock;
Expand All @@ -58,7 +71,15 @@ describe("Telemetry", () => {
expect(appendEvents.length).toBe(appendEventsCalls);

if (sendEventsCalledWith) {
expect(sendEvents[0]?.[0]).toEqual(sendEventsCalledWith);
expect(sendEvents[0]?.[0]).toEqual(
sendEventsCalledWith.map((event) => ({
...event,
properties: {
...telemetry.getCommonProperties(),
...event.properties,
},
}))
);
}

if (appendEventsCalledWith) {
Expand All @@ -71,7 +92,7 @@ describe("Telemetry", () => {
jest.clearAllMocks();

// Setup mocked API client
mockApiClient = new MockApiClient() as jest.Mocked<ApiClient>;
mockApiClient = new MockApiClient({ baseUrl: "" }) as jest.Mocked<ApiClient>;
mockApiClient.sendEvents = jest.fn().mockResolvedValue(undefined);
mockApiClient.hasCredentials = jest.fn().mockReturnValue(true);

Expand Down
19 changes: 19 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"types": ["node", "jest"],
"sourceMap": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"typeRoots": ["./node_modules/@types", "./src/types"]
},
"include": ["src/**/*.ts"]
}
2 changes: 1 addition & 1 deletion tsconfig.jest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "./tsconfig.json",
"extends": "./tsconfig.build.json",
"compilerOptions": {
"module": "esnext",
"target": "esnext",
Expand Down
Loading
Loading