diff --git a/.github/workflows/ci-mcp.yml b/.github/workflows/ci-mcp.yml index 0f1de07621..d590b78932 100644 --- a/.github/workflows/ci-mcp.yml +++ b/.github/workflows/ci-mcp.yml @@ -26,47 +26,6 @@ jobs: - '.github/workflows/mcp-ci.yml' - '.github/workflows/mcp-publish.yml' - lint-and-format: - name: Lint, Format, and Type Check - runs-on: ubuntu-latest - needs: changes - if: needs.changes.outputs.mcp == 'true' - permissions: - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Run linter - run: cd products/mcp && pnpm run lint - - - name: Run formatter - run: cd products/mcp && pnpm run format - - - name: Run type check - run: cd products/mcp && pnpm run typecheck - - - name: Check for changes - run: | - if [ -n "$(git status --porcelain)" ]; then - echo "Code formatting or linting changes detected!" - git diff - exit 1 - fi - unit-tests: name: Unit Tests runs-on: ubuntu-latest diff --git a/.oxlintrc.json b/.oxlintrc.json index c0c848f72c..168a08e478 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -16,7 +16,10 @@ "common/plugin_transpiler/dist", "common/hogvm/__tests__/__snapshots__", "common/hogvm/__tests__/__snapshots__/**", - "frontend/src/queries/validators.js" + "frontend/src/queries/validators.js", + "products/mcp/**/generated.ts", + "products/mcp/schema/tool-inputs.json", + "products/mcp/python" ], "rules": { "no-constant-condition": "off", diff --git a/.prettierignore b/.prettierignore index 17e003ff21..baf0728a48 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ staticfiles .env *.code-workspace .mypy_cache +.cache/ *Type.ts .idea .yalc @@ -15,8 +16,7 @@ common/storybook/dist/ dist/ node_modules/ pnpm-lock.yaml -posthog/templates/email/* -posthog/templates/**/*.html +posthog/templates/**/* common/hogvm/typescript/src/stl/bytecode.ts common/hogvm/__tests__/__snapshots__/* rust/ @@ -26,3 +26,10 @@ cli/tests/_cases/**/* frontend/src/products.tsx frontend/src/layout.html **/fixtures/** +products/mcp/schema/**/* +products/mcp/python/**/* +products/mcp/**/generated.ts +products/mcp/typescript/worker-configuration.d.ts +frontend/src/taxonomy/core-filter-definitions-by-group.json +frontend/dist/ +frontend/**/*LogicType.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 16593b9f80..ae2e06e384 100644 --- a/.prettierrc +++ b/.prettierrc @@ -14,6 +14,7 @@ "^@posthog.*$", "^lib/(.*)$|^scenes/(.*)$", "^~/(.*)$", + "^@/(.*)$", "^public/(.*)$", "^products/(.*)$", "^storybook/(.*)$", diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 13fd6db253..0000000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -.cache/ -src/taxonomy/core-filter-definitions-by-group.json -dist/ -*LogicType.ts diff --git a/frontend/package.json b/frontend/package.json index 9ab60c8cb2..99be5d76e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,8 +37,8 @@ "schema:build:json": "ts-node ./bin/build-schema-json.mjs && prettier --write ./src/queries/schema.json && ts-node ./bin/build-validators.mjs && prettier --write ./src/queries/validators.js", "test": "SHARD_IDX=${SHARD_INDEX:-1}; SHARD_TOTAL=${SHARD_COUNT:-1}; echo $SHARD_IDX/$SHARD_TOTAL; pnpm build:products && jest --testPathPattern='(frontend/|products/|common/)' --runInBand --forceExit --shard=$SHARD_IDX/$SHARD_TOTAL", "jest": "pnpm build:products && jest", - "prettier": "prettier --write \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"", - "prettier:check": "prettier --check \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"", + "prettier": "prettier --write \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\" --ignore-path \"../.prettierignore\"", + "prettier:check": "prettier --check \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\" --ignore-path \"../.prettierignore\"", "typescript:check": "tsc --noEmit && echo \"No errors reported by tsc.\"", "lint": "pnpm lint:js && pnpm lint:css", "lint:js": "cd .. && oxlint --quiet", diff --git a/products/mcp/.oxlintrc.json b/products/mcp/.oxlintrc.json deleted file mode 100644 index 749789b4d2..0000000000 --- a/products/mcp/.oxlintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "files": { - "ignore": ["**/generated.ts", "schema/tool-inputs.json", "python/**", ".mypy_cache/**"] - }, - "rules": { - "no-debugger": "off", - "no-console": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "jest/no-restricted-matchers": "off" - } -} diff --git a/products/mcp/.prettierignore b/products/mcp/.prettierignore deleted file mode 100644 index ec4a4d6a47..0000000000 --- a/products/mcp/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -schema/tool-inputs.json -python/** -**/generated.ts -node_modules/ -.mypy_cache/ diff --git a/products/mcp/.prettierrc b/products/mcp/.prettierrc deleted file mode 100644 index 932ed3ef04..0000000000 --- a/products/mcp/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 4, - "semi": false, - "singleQuote": true, - "printWidth": 100 -} diff --git a/products/mcp/README.md b/products/mcp/README.md index 9fd04bb311..c48e39de31 100644 --- a/products/mcp/README.md +++ b/products/mcp/README.md @@ -20,21 +20,21 @@ npx @posthog/wizard@latest mcp add ```json { - "mcpServers": { - "posthog": { - "command": "npx", - "args": [ - "-y", - "mcp-remote@latest", - "https://mcp.posthog.com/mcp", // You can replace this with https://mcp.posthog.com/sse if your client does not support Streamable HTTP - "--header", - "Authorization:${POSTHOG_AUTH_HEADER}" - ], - "env": { - "POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}" - } - } + "mcpServers": { + "posthog": { + "command": "npx", + "args": [ + "-y", + "mcp-remote@latest", + "https://mcp.posthog.com/mcp", // You can replace this with https://mcp.posthog.com/sse if your client does not support Streamable HTTP + "--header", + "Authorization:${POSTHOG_AUTH_HEADER}" + ], + "env": { + "POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}" + } } + } } ``` @@ -51,27 +51,27 @@ If you want to call MCP from Node (outside an IDE), use the Model Context Protoc import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' -import { URL } from 'node:url' import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' +import { URL } from 'node:url' const AUTH = process.env.POSTHOG_AUTH_HEADER // "Bearer phx_…" const MCP_URL = process.env.MCP_URL || 'https://mcp.posthog.com/mcp' if (!AUTH?.startsWith('Bearer ')) { - console.error('Set POSTHOG_AUTH_HEADER="Bearer phx_..."') - process.exit(1) + console.error('Set POSTHOG_AUTH_HEADER="Bearer phx_..."') + process.exit(1) } const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), { - requestInit: { - headers: { - Authorization: AUTH, - // Required for Streamable HTTP (JSON + SSE) - Accept: 'application/json, text/event-stream', - }, + requestInit: { + headers: { + Authorization: AUTH, + // Required for Streamable HTTP (JSON + SSE) + Accept: 'application/json, text/event-stream', }, - serverInfo: { name: 'example-node-client', version: '0.0.1' }, + }, + serverInfo: { name: 'example-node-client', version: '0.0.1' }, }) const client = new Client({ name: 'example-node-client', version: '0.0.1' }) @@ -115,26 +115,26 @@ docker build -t posthog-mcp . ```json { - "mcpServers": { - "posthog": { - "type": "stdio", - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "--env", - "POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER}", - "--env", - "POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp}", - "posthog-mcp" - ], - "env": { - "POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}", - "POSTHOG_REMOTE_MCP_URL": "https://mcp.posthog.com/mcp" - } - } + "mcpServers": { + "posthog": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--env", + "POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER}", + "--env", + "POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp}", + "posthog-mcp" + ], + "env": { + "POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}", + "POSTHOG_REMOTE_MCP_URL": "https://mcp.posthog.com/mcp" + } } + } } ``` @@ -239,14 +239,5 @@ Alternatively, you can use the following configuration in the MCP Inspector: Use transport type `STDIO`. -**Command:** - -```bash -npx -``` - -**Arguments:** - -```bash --y mcp-remote@latest http://localhost:8787/mcp --header "Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}" -``` +- **Command**: `npx` +- **Arguments**: `-y mcp-remote@latest http://localhost:8787/mcp --header "Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}"` diff --git a/products/mcp/examples/ai-sdk/src/index.ts b/products/mcp/examples/ai-sdk/src/index.ts index 6bd32d2eb7..eef7e9cc36 100644 --- a/products/mcp/examples/ai-sdk/src/index.ts +++ b/products/mcp/examples/ai-sdk/src/index.ts @@ -1,15 +1,16 @@ import { openai } from '@ai-sdk/openai' -import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk' import { generateText, stepCountIs } from 'ai' import 'dotenv/config' -async function analyzeProductUsage() { +import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk' + +async function analyzeProductUsage(): Promise { const agentToolkit = new PostHogAgentToolkit({ posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, posthogApiBaseUrl: process.env.POSTHOG_API_BASE_URL || 'https://us.posthog.com', }) - const result = await generateText({ + await generateText({ model: openai('gpt-5-mini'), tools: await agentToolkit.getTools(), stopWhen: stepCountIs(30), @@ -24,7 +25,7 @@ async function analyzeProductUsage() { }) } -async function main() { +async function main(): Promise { try { await analyzeProductUsage() } catch (error) { diff --git a/products/mcp/examples/langchain-js/src/index.ts b/products/mcp/examples/langchain-js/src/index.ts index 8fa1b143c4..a7d8500274 100644 --- a/products/mcp/examples/langchain-js/src/index.ts +++ b/products/mcp/examples/langchain-js/src/index.ts @@ -1,10 +1,11 @@ import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts' import { ChatOpenAI } from '@langchain/openai' -import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain' -import { AgentExecutor, createToolCallingAgent } from 'langchain/agents' import 'dotenv/config' +import { AgentExecutor, createToolCallingAgent } from 'langchain/agents' -async function analyzeProductUsage() { +import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain' + +async function analyzeProductUsage(): Promise { const agentToolkit = new PostHogAgentToolkit({ posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, posthogApiBaseUrl: process.env.POSTHOG_API_BASE_URL || 'https://us.posthog.com', @@ -38,7 +39,7 @@ async function analyzeProductUsage() { maxIterations: 5, }) - const result = await agentExecutor.invoke({ + await agentExecutor.invoke({ input: `Please analyze our product usage: 1. Get all available insights (limit 100) and pick the 5 most relevant ones @@ -49,7 +50,7 @@ async function analyzeProductUsage() { }) } -async function main() { +async function main(): Promise { try { await analyzeProductUsage() } catch (error) { diff --git a/products/mcp/examples/langchain/README.md b/products/mcp/examples/langchain/README.md index a162c9bc7b..cf8ba901e0 100644 --- a/products/mcp/examples/langchain/README.md +++ b/products/mcp/examples/langchain/README.md @@ -19,8 +19,8 @@ cp .env.example .env ``` 3. Update your `.env` file with: - - `POSTHOG_PERSONAL_API_KEY`: Your PostHog personal API key - - `OPENAI_API_KEY`: Your OpenAI API key + - `POSTHOG_PERSONAL_API_KEY`: Your PostHog personal API key + - `OPENAI_API_KEY`: Your OpenAI API key ## Usage @@ -38,9 +38,9 @@ The example will: 2. Load all available PostHog tools from the MCP server 3. Create a LangChain agent with access to PostHog data 4. Analyze product usage by: - - Getting available insights - - Querying data for the most relevant ones - - Summarizing key findings + - Getting available insights + - Querying data for the most relevant ones + - Summarizing key findings ## Available Tools diff --git a/products/mcp/package.json b/products/mcp/package.json index 06122664a1..200f783f27 100644 --- a/products/mcp/package.json +++ b/products/mcp/package.json @@ -1,5 +1,5 @@ { - "name": "posthog-mcp-monorepo", + "name": "mcp", "version": "0.0.0", "private": true, "scripts": { @@ -11,13 +11,7 @@ "test:integration": "cd typescript && pnpm test:integration", "schema:build:python": "bash python/scripts/generate-pydantic-models.sh", "schema:build": "pnpm run schema:build:json && pnpm run schema:build:python", - "format": "prettier --write . && oxlint --fix --fix-suggestions --quiet .", - "lint": "prettier --check . && oxlint .", - "format:python": "cd python && uv run ruff format .", - "lint:python": "cd python && uv run ruff check --fix .", "test:python": "cd python && uv run pytest tests/ -v", - "typecheck": "cd typescript && pnpm typecheck", - "typecheck:python": "cd python && uvx ty check", "docker:build": "docker build -t posthog-mcp .", "docker:run": "docker run -i --rm --env POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER} --env POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp} posthog-mcp", "docker:inspector": "npx @modelcontextprotocol/inspector docker run -i --rm --env POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER} --env POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp} posthog-mcp" diff --git a/products/mcp/typescript/README.md b/products/mcp/typescript/README.md index 1f46f2bd5a..821b2bfa57 100644 --- a/products/mcp/typescript/README.md +++ b/products/mcp/typescript/README.md @@ -21,14 +21,15 @@ import { generateText } from 'ai' import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk' const toolkit = new PostHogAgentToolkit({ - posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, - posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU + posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, + posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU }) const result = await generateText({ - model: openai('gpt-4'), - tools: await toolkit.getTools(), - prompt: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.', + model: openai('gpt-4'), + tools: await toolkit.getTools(), + prompt: + 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.', }) ``` @@ -44,24 +45,24 @@ import { AgentExecutor, createToolCallingAgent } from 'langchain/agents' import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain' const toolkit = new PostHogAgentToolkit({ - posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, - posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU + posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!, + posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU }) const tools = await toolkit.getTools() const llm = new ChatOpenAI({ model: 'gpt-4' }) const prompt = ChatPromptTemplate.fromMessages([ - ['system', 'You are a data analyst with access to PostHog analytics'], - ['human', '{input}'], - new MessagesPlaceholder('agent_scratchpad'), + ['system', 'You are a data analyst with access to PostHog analytics'], + ['human', '{input}'], + new MessagesPlaceholder('agent_scratchpad'), ]) const agent = createToolCallingAgent({ llm, tools, prompt }) const executor = new AgentExecutor({ agent, tools }) const result = await executor.invoke({ - input: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.', + input: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.', }) ``` diff --git a/products/mcp/typescript/scripts/generate-tool-schema.ts b/products/mcp/typescript/scripts/generate-tool-schema.ts index 3433f02cb0..b02dce86a3 100755 --- a/products/mcp/typescript/scripts/generate-tool-schema.ts +++ b/products/mcp/typescript/scripts/generate-tool-schema.ts @@ -1,10 +1,9 @@ #!/usr/bin/env tsx - // Generates JSON schema from Zod tool-inputs schemas for Python Pydantic schema generation - import * as fs from 'node:fs' import * as path from 'node:path' import { zodToJsonSchema } from 'zod-to-json-schema' + import * as schemas from '../src/schema/tool-inputs' const outputPath = path.join(__dirname, '../../schema/tool-inputs.json') diff --git a/products/mcp/typescript/scripts/update-openapi-client.ts b/products/mcp/typescript/scripts/update-openapi-client.ts index fff5611490..923801ee25 100644 --- a/products/mcp/typescript/scripts/update-openapi-client.ts +++ b/products/mcp/typescript/scripts/update-openapi-client.ts @@ -1,5 +1,4 @@ #!/usr/bin/env tsx - import { execSync } from 'node:child_process' import * as fs from 'node:fs' @@ -37,7 +36,7 @@ function generateClient() { } } -function cleanup() { +function cleanup(): void { try { if (fs.existsSync(TEMP_SCHEMA_PATH)) { fs.unlinkSync(TEMP_SCHEMA_PATH) @@ -47,7 +46,7 @@ function cleanup() { } } -async function main() { +async function main(): Promise { const schemaFetched = await fetchSchema() if (!schemaFetched) { process.exit(1) diff --git a/products/mcp/typescript/src/api/client.ts b/products/mcp/typescript/src/api/client.ts index 47501b17cd..df66132c04 100644 --- a/products/mcp/typescript/src/api/client.ts +++ b/products/mcp/typescript/src/api/client.ts @@ -1,3 +1,5 @@ +import { z } from 'zod' + import { ErrorCode } from '@/lib/errors' import { withPagination } from '@/lib/utils/api' import { getSearchParamsFromRecord } from '@/lib/utils/helper-functions' @@ -51,7 +53,7 @@ import { type Organization, OrganizationSchema } from '@/schema/orgs' import { type Project, ProjectSchema } from '@/schema/projects' import type { ExperimentCreateSchema } from '@/schema/tool-inputs' import { isShortId } from '@/tools/insights/utils' -import { z } from 'zod' + import type { CreateSurveyInput, GetSurveySpecificStatsInput, @@ -81,6 +83,9 @@ export interface ApiConfig { apiToken: string baseUrl: string } + +type Endpoint = Record + export class ApiClient { private config: ApiConfig private baseUrl: string @@ -93,14 +98,15 @@ export class ApiClient { this.generated = createApiClient(buildApiFetcher(this.config), this.baseUrl) } - private buildHeaders() { + + private buildHeaders(): Record { return { Authorization: `Bearer ${this.config.apiToken}`, 'Content-Type': 'application/json', } } - getProjectBaseUrl(projectId: string) { + getProjectBaseUrl(projectId: string): string { if (projectId === '@current') { return this.baseUrl } @@ -108,11 +114,7 @@ export class ApiClient { return `${this.baseUrl}/project/${projectId}` } - private async fetchWithSchema( - url: string, - schema: z.ZodType, - options?: RequestInit - ): Promise> { + private async fetchWithSchema(url: string, schema: z.ZodType, options?: RequestInit): Promise> { try { const response = await fetch(url, { ...options, @@ -158,17 +160,14 @@ export class ApiClient { } } - organizations() { + organizations(): Endpoint { return { list: async (): Promise> => { const responseSchema = z.object({ results: z.array(OrganizationSchema), }) - const result = await this.fetchWithSchema( - `${this.baseUrl}/api/organizations/`, - responseSchema - ) + const result = await this.fetchWithSchema(`${this.baseUrl}/api/organizations/`, responseSchema) if (result.success) { return { success: true, data: result.data.results } @@ -177,10 +176,7 @@ export class ApiClient { }, get: async ({ orgId }: { orgId: string }): Promise> => { - return this.fetchWithSchema( - `${this.baseUrl}/api/organizations/${orgId}/`, - OrganizationSchema - ) + return this.fetchWithSchema(`${this.baseUrl}/api/organizations/${orgId}/`, OrganizationSchema) }, projects: ({ orgId }: { orgId: string }) => { @@ -205,7 +201,7 @@ export class ApiClient { } } - apiKeys() { + apiKeys(): Endpoint { return { current: async (): Promise> => { return this.fetchWithSchema( @@ -216,13 +212,10 @@ export class ApiClient { } } - projects() { + projects(): Endpoint { return { get: async ({ projectId }: { projectId: string }): Promise> => { - return this.fetchWithSchema( - `${this.baseUrl}/api/projects/${projectId}/`, - ProjectSchema - ) + return this.fetchWithSchema(`${this.baseUrl}/api/projects/${projectId}/`, ProjectSchema) }, propertyDefinitions: async ({ @@ -268,9 +261,7 @@ export class ApiClient { ApiPropertyDefinitionSchema ) - const propertyDefinitionsWithoutHidden = propertyDefinitions.filter( - (def) => !def.hidden - ) + const propertyDefinitionsWithoutHidden = propertyDefinitions.filter((def) => !def.hidden) return { success: true, data: propertyDefinitionsWithoutHidden } } catch (error) { @@ -304,7 +295,7 @@ export class ApiClient { } } - experiments({ projectId }: { projectId: string }) { + experiments({ projectId }: { projectId: string }): Endpoint { return { list: async (): Promise> => { try { @@ -320,11 +311,7 @@ export class ApiClient { } }, - get: async ({ - experimentId, - }: { - experimentId: number - }): Promise> => { + get: async ({ experimentId }: { experimentId: number }): Promise> => { return this.fetchWithSchema( `${this.baseUrl}/api/projects/${projectId}/experiments/${experimentId}/`, ExperimentSchema @@ -474,10 +461,7 @@ export class ApiClient { const sharedSecondaryMetrics = (experiment.saved_metrics || []) .filter(({ metadata }) => metadata.type === 'secondary') .map(({ query }) => query) - const allSecondaryMetrics = [ - ...(experiment.metrics_secondary || []), - ...sharedSecondaryMetrics, - ] + const allSecondaryMetrics = [...(experiment.metrics_secondary || []), ...sharedSecondaryMetrics] // Execute queries for primary metrics const primaryResults = await Promise.all( @@ -504,7 +488,7 @@ export class ApiClient { ) return result.success ? result.data : null - } catch (error) { + } catch { return null } }) @@ -535,7 +519,7 @@ export class ApiClient { ) return result.success ? result.data : null - } catch (error) { + } catch { return null } }) @@ -552,9 +536,7 @@ export class ApiClient { } }, - create: async ( - experimentData: z.infer - ): Promise> => { + create: async (experimentData: z.infer): Promise> => { // Transform agent input to API payload const createBody = ExperimentCreatePayloadSchema.parse(experimentData) @@ -624,11 +606,9 @@ export class ApiClient { } } - featureFlags({ projectId }: { projectId: string }) { + featureFlags({ projectId }: { projectId: string }): Endpoint { return { - list: async (): Promise< - Result> - > => { + list: async (): Promise>> => { try { const schema = FeatureFlagSchema.pick({ id: true, @@ -680,9 +660,7 @@ export class ApiClient { key, }: { key: string - }): Promise< - Result<{ id: number; key: string; name: string; active: boolean } | undefined> - > => { + }): Promise> => { const listResult = await this.featureFlags({ projectId }).list() if (!listResult.success) { @@ -768,20 +746,13 @@ export class ApiClient { ) }, - delete: async ({ - flagId, - }: { - flagId: number - }): Promise> => { + delete: async ({ flagId }: { flagId: number }): Promise> => { try { - const response = await fetch( - `${this.baseUrl}/api/projects/${projectId}/feature_flags/${flagId}/`, - { - method: 'PATCH', - headers: this.buildHeaders(), - body: JSON.stringify({ deleted: true }), - } - ) + const response = await fetch(`${this.baseUrl}/api/projects/${projectId}/feature_flags/${flagId}/`, { + method: 'PATCH', + headers: this.buildHeaders(), + body: JSON.stringify({ deleted: true }), + }) if (!response.ok) { throw new Error(`Failed to delete feature flag: ${response.statusText}`) @@ -801,26 +772,21 @@ export class ApiClient { } } - insights({ projectId }: { projectId: string }) { + insights({ projectId }: { projectId: string }): Endpoint { return { - list: async ({ params }: { params?: ListInsightsData } = {}): Promise< - Result> - > => { + list: async ({ params }: { params?: ListInsightsData } = {}): Promise>> => { try { - const response = await this.generated.get( - '/api/projects/{project_id}/insights/', - { - path: { project_id: projectId }, - query: params - ? { - limit: params.limit, - offset: params.offset, - //@ts-expect-error search is not implemented as a query parameter - search: params.search, - } - : {}, - } - ) + const response = await this.generated.get('/api/projects/{project_id}/insights/', { + path: { project_id: projectId }, + query: params + ? { + limit: params.limit, + offset: params.offset, + //@ts-expect-error search is not implemented as a query parameter + search: params.search, + } + : {}, + }) return { success: true, data: response.results } } catch (error) { @@ -828,11 +794,7 @@ export class ApiClient { } }, - create: async ({ - data, - }: { - data: CreateInsightInput - }): Promise> => { + create: async ({ data }: { data: CreateInsightInput }): Promise> => { const validatedInput = CreateInsightInputSchema.parse(data) return this.fetchWithSchema( @@ -881,13 +843,7 @@ export class ApiClient { ) }, - update: async ({ - insightId, - data, - }: { - insightId: number - data: any - }): Promise> => { + update: async ({ insightId, data }: { insightId: number; data: any }): Promise> => { return this.fetchWithSchema( `${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`, SimpleInsightSchema, @@ -904,14 +860,11 @@ export class ApiClient { insightId: number }): Promise> => { try { - const response = await fetch( - `${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`, - { - method: 'PATCH', - headers: this.buildHeaders(), - body: JSON.stringify({ deleted: true }), - } - ) + const response = await fetch(`${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`, { + method: 'PATCH', + headers: this.buildHeaders(), + body: JSON.stringify({ deleted: true }), + }) if (!response.ok) { throw new Error(`Failed to delete insight: ${response.statusText}`) @@ -976,7 +929,7 @@ export class ApiClient { } } - dashboards({ projectId }: { projectId: string }) { + dashboards({ projectId }: { projectId: string }): Endpoint { return { list: async ({ params }: { params?: ListDashboardsData } = {}): Promise< Result< @@ -1021,22 +974,14 @@ export class ApiClient { return result }, - get: async ({ - dashboardId, - }: { - dashboardId: number - }): Promise> => { + get: async ({ dashboardId }: { dashboardId: number }): Promise> => { return this.fetchWithSchema( `${this.baseUrl}/api/projects/${projectId}/dashboards/${dashboardId}/`, SimpleDashboardSchema ) }, - create: async ({ - data, - }: { - data: CreateDashboardInput - }): Promise> => { + create: async ({ data }: { data: CreateDashboardInput }): Promise> => { const validatedInput = CreateDashboardInputSchema.parse(data) const createResponseSchema = z.object({ @@ -1124,36 +1069,25 @@ export class ApiClient { } } - query({ projectId }: { projectId: string }) { + query({ projectId }: { projectId: string }): Endpoint { return { - execute: async ({ - queryBody, - }: { - queryBody: any - }): Promise> => { + execute: async ({ queryBody }: { queryBody: any }): Promise> => { const responseSchema = z.object({ results: z.array(z.any()), }) - return this.fetchWithSchema( - `${this.baseUrl}/api/environments/${projectId}/query/`, - responseSchema, - { - method: 'POST', - body: JSON.stringify({ query: queryBody }), - } - ) + return this.fetchWithSchema(`${this.baseUrl}/api/environments/${projectId}/query/`, responseSchema, { + method: 'POST', + body: JSON.stringify({ query: queryBody }), + }) }, } } - users() { + users(): Endpoint { return { me: async (): Promise> => { - const result = await this.fetchWithSchema( - `${this.baseUrl}/api/users/@me/`, - ApiUserSchema - ) + const result = await this.fetchWithSchema(`${this.baseUrl}/api/users/@me/`, ApiUserSchema) if (!result.success) { return result @@ -1167,7 +1101,7 @@ export class ApiClient { } } - surveys({ projectId }: { projectId: string }) { + surveys({ projectId }: { projectId: string }): Endpoint { return { list: async ({ params }: { params?: ListSurveysInput } = {}): Promise< Result> @@ -1207,21 +1141,13 @@ export class ApiClient { ) }, - create: async ({ - data, - }: { - data: CreateSurveyInput - }): Promise> => { + create: async ({ data }: { data: CreateSurveyInput }): Promise> => { const validatedInput = CreateSurveyInputSchema.parse(data) - return this.fetchWithSchema( - `${this.baseUrl}/api/projects/${projectId}/surveys/`, - SurveyOutputSchema, - { - method: 'POST', - body: JSON.stringify(validatedInput), - } - ) + return this.fetchWithSchema(`${this.baseUrl}/api/projects/${projectId}/surveys/`, SurveyOutputSchema, { + method: 'POST', + body: JSON.stringify(validatedInput), + }) }, update: async ({ @@ -1266,9 +1192,7 @@ export class ApiClient { ) if (!response.ok) { - throw new Error( - `Failed to ${softDelete ? 'archive' : 'delete'} survey: ${response.statusText}` - ) + throw new Error(`Failed to ${softDelete ? 'archive' : 'delete'} survey: ${response.statusText}`) } return { @@ -1295,9 +1219,7 @@ export class ApiClient { return this.fetchWithSchema(url, SurveyResponseStatsOutputSchema) }, - stats: async ( - params: GetSurveySpecificStatsInput - ): Promise> => { + stats: async (params: GetSurveySpecificStatsInput): Promise> => { const validatedParams = GetSurveySpecificStatsInputSchema.parse(params) const searchParams = getSearchParamsFromRecord(validatedParams) diff --git a/products/mcp/typescript/src/api/fetcher.ts b/products/mcp/typescript/src/api/fetcher.ts index 93c1fb61e3..2e0bc6d230 100644 --- a/products/mcp/typescript/src/api/fetcher.ts +++ b/products/mcp/typescript/src/api/fetcher.ts @@ -1,9 +1,7 @@ import type { ApiConfig } from './client' import type { createApiClient } from './generated' -export const buildApiFetcher: (config: ApiConfig) => Parameters[0] = ( - config -) => { +export const buildApiFetcher: (config: ApiConfig) => Parameters[0] = (config) => { return { fetch: async (input) => { const headers = new Headers() @@ -41,9 +39,7 @@ export const buildApiFetcher: (config: ApiConfig) => Parameters 0 && - data.choices[0]?.message && - data.choices[0].message.content - ) { + if (data.choices && data.choices.length > 0 && data.choices[0]?.message && data.choices[0].message.content) { return data.choices[0].message.content } console.error('Inkeep API response format unexpected:', data) diff --git a/products/mcp/typescript/src/integrations/ai-sdk/index.ts b/products/mcp/typescript/src/integrations/ai-sdk/index.ts index 3195b62692..3e0394df24 100644 --- a/products/mcp/typescript/src/integrations/ai-sdk/index.ts +++ b/products/mcp/typescript/src/integrations/ai-sdk/index.ts @@ -1,3 +1,6 @@ +import { type Tool as VercelTool, tool } from 'ai' +import type { z } from 'zod' + import { ApiClient } from '@/api/client' import { SessionManager } from '@/lib/utils/SessionManager' import { StateManager } from '@/lib/utils/StateManager' @@ -5,8 +8,6 @@ import { MemoryCache } from '@/lib/utils/cache/MemoryCache' import { hash } from '@/lib/utils/helper-functions' import { getToolsFromContext } from '@/tools' import type { Context } from '@/tools/types' -import { type Tool as VercelTool, tool } from 'ai' -import type { z } from 'zod' /** * Options for the PostHog Agent Toolkit diff --git a/products/mcp/typescript/src/integrations/langchain/index.ts b/products/mcp/typescript/src/integrations/langchain/index.ts index 040d499e41..2ab9a587dc 100644 --- a/products/mcp/typescript/src/integrations/langchain/index.ts +++ b/products/mcp/typescript/src/integrations/langchain/index.ts @@ -1,3 +1,6 @@ +import { DynamicStructuredTool } from '@langchain/core/tools' +import type { z } from 'zod' + import { ApiClient } from '@/api/client' import { SessionManager } from '@/lib/utils/SessionManager' import { StateManager } from '@/lib/utils/StateManager' @@ -5,8 +8,6 @@ import { MemoryCache } from '@/lib/utils/cache/MemoryCache' import { hash } from '@/lib/utils/helper-functions' import { getToolsFromContext } from '@/tools' import type { Context } from '@/tools/types' -import { DynamicStructuredTool } from '@langchain/core/tools' -import type { z } from 'zod' /** * Options for the PostHog Agent Toolkit diff --git a/products/mcp/typescript/src/integrations/mcp/index.ts b/products/mcp/typescript/src/integrations/mcp/index.ts index c7f452e44c..83541398f8 100644 --- a/products/mcp/typescript/src/integrations/mcp/index.ts +++ b/products/mcp/typescript/src/integrations/mcp/index.ts @@ -51,26 +51,23 @@ export class MyMCP extends McpAgent { _sessionManager: SessionManager | undefined - get requestProperties() { + get requestProperties(): RequestProperties { return this.props as RequestProperties } - get cache() { + get cache(): DurableObjectCache { if (!this.requestProperties.userHash) { throw new Error('User hash is required to use the cache') } if (!this._cache) { - this._cache = new DurableObjectCache( - this.requestProperties.userHash, - this.ctx.storage - ) + this._cache = new DurableObjectCache(this.requestProperties.userHash, this.ctx.storage) } return this._cache } - get sessionManager() { + get sessionManager(): SessionManager { if (!this._sessionManager) { this._sessionManager = new SessionManager(this.cache) } @@ -89,10 +86,7 @@ export class MyMCP extends McpAgent { baseUrl: 'https://eu.posthog.com', }) - const [usResult, euResult] = await Promise.all([ - usClient.users().me(), - euClient.users().me(), - ]) + const [usResult, euResult] = await Promise.all([usClient.users().me(), euClient.users().me()]) if (usResult.success) { await this.cache.set('region', 'us') @@ -107,7 +101,7 @@ export class MyMCP extends McpAgent { return undefined } - async getBaseUrl() { + async getBaseUrl(): Promise { if (CUSTOM_BASE_URL) { return CUSTOM_BASE_URL } @@ -121,7 +115,7 @@ export class MyMCP extends McpAgent { return 'https://us.posthog.com' } - async api() { + async api(): Promise { if (!this._api) { const baseUrl = await this.getBaseUrl() this._api = new ApiClient({ @@ -133,7 +127,7 @@ export class MyMCP extends McpAgent { return this._api } - async getDistinctId() { + async getDistinctId(): Promise { let _distinctId = await this.cache.get('distinctId') if (!_distinctId) { @@ -148,7 +142,7 @@ export class MyMCP extends McpAgent { return _distinctId } - async trackEvent(event: AnalyticsEvent, properties: Record = {}) { + async trackEvent(event: AnalyticsEvent, properties: Record = {}): Promise { try { const distinctId = await this.getDistinctId() @@ -160,16 +154,14 @@ export class MyMCP extends McpAgent { properties: { ...(this.requestProperties.sessionId ? { - $session_id: await this.sessionManager.getSessionUuid( - this.requestProperties.sessionId - ), + $session_id: await this.sessionManager.getSessionUuid(this.requestProperties.sessionId), } : {}), ...properties, }, }) - } catch (error) { - // + } catch { + // skip } } @@ -177,7 +169,7 @@ export class MyMCP extends McpAgent { tool: Tool>, handler: (params: z.infer>) => Promise ): void { - const wrappedHandler = async (params: z.infer>) => { + const wrappedHandler = async (params: z.infer>): Promise => { const validation = tool.schema.safeParse(params) if (!validation.success) { @@ -245,7 +237,7 @@ export class MyMCP extends McpAgent { } } - async init() { + async init(): Promise { const context = await this.getContext() // Register prompts and resources diff --git a/products/mcp/typescript/src/integrations/mcp/utils/client.ts b/products/mcp/typescript/src/integrations/mcp/utils/client.ts index 4b1ff7f2de..9b33c0363f 100644 --- a/products/mcp/typescript/src/integrations/mcp/utils/client.ts +++ b/products/mcp/typescript/src/integrations/mcp/utils/client.ts @@ -2,7 +2,7 @@ import { PostHog } from 'posthog-node' let _client: PostHog | undefined -export const getPostHogClient = () => { +export const getPostHogClient = (): PostHog => { if (!_client) { _client = new PostHog('sTMFPsFhdP1Ssg', { host: 'https://us.i.posthog.com', diff --git a/products/mcp/typescript/src/integrations/mcp/utils/handleToolError.ts b/products/mcp/typescript/src/integrations/mcp/utils/handleToolError.ts index 34a71afd75..41b1a35900 100644 --- a/products/mcp/typescript/src/integrations/mcp/utils/handleToolError.ts +++ b/products/mcp/typescript/src/integrations/mcp/utils/handleToolError.ts @@ -1,6 +1,7 @@ -import { getPostHogClient } from '@/integrations/mcp/utils/client' import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import { getPostHogClient } from '@/integrations/mcp/utils/client' + export class MCPToolError extends Error { public readonly tool: string public readonly originalError: unknown @@ -13,22 +14,6 @@ export class MCPToolError extends Error { this.originalError = originalError this.timestamp = new Date() } - - getTrackingData() { - return { - tool: this.tool, - message: this.message, - timestamp: this.timestamp.toISOString(), - originalError: - this.originalError instanceof Error - ? { - name: this.originalError.name, - message: this.originalError.message, - stack: this.originalError.stack, - } - : String(this.originalError), - } - } } /** @@ -45,20 +30,11 @@ export class MCPToolError extends Error { * * @returns A structured error message. */ -export function handleToolError( - error: any, - tool?: string, - distinctId?: string, - sessionUuid?: string -): CallToolResult { +export function handleToolError(error: any, tool?: string, distinctId?: string, sessionUuid?: string): CallToolResult { const mcpError = error instanceof MCPToolError ? error - : new MCPToolError( - error instanceof Error ? error.message : String(error), - tool || 'unknown', - error - ) + : new MCPToolError(error instanceof Error ? error.message : String(error), tool || 'unknown', error) const properties: Record = { team: 'growth', diff --git a/products/mcp/typescript/src/lib/utils/SessionManager.ts b/products/mcp/typescript/src/lib/utils/SessionManager.ts index 1124f998c7..0d54084f88 100644 --- a/products/mcp/typescript/src/lib/utils/SessionManager.ts +++ b/products/mcp/typescript/src/lib/utils/SessionManager.ts @@ -1,7 +1,8 @@ +import { v7 as uuidv7 } from 'uuid' + import type { PrefixedString } from '@/lib/types' import type { ScopedCache } from '@/lib/utils/cache/ScopedCache' import type { State } from '@/tools' -import { v7 as uuidv7 } from 'uuid' export class SessionManager { private cache: ScopedCache diff --git a/products/mcp/typescript/src/lib/utils/StateManager.ts b/products/mcp/typescript/src/lib/utils/StateManager.ts index d4f0360f02..2ba9ebc820 100644 --- a/products/mcp/typescript/src/lib/utils/StateManager.ts +++ b/products/mcp/typescript/src/lib/utils/StateManager.ts @@ -1,6 +1,7 @@ import type { ApiClient } from '@/api/client' import type { ApiUser } from '@/schema/api' import type { State } from '@/tools/types' + import type { ScopedCache } from './cache/ScopedCache' export class StateManager { @@ -13,7 +14,7 @@ export class StateManager { this._api = api } - private async _fetchUser() { + private async _fetchUser(): Promise { const userResult = await this._api.users().me() if (!userResult.success) { throw new Error(`Failed to get user: ${userResult.error.message}`) @@ -21,7 +22,7 @@ export class StateManager { return userResult.data } - async getUser() { + async getUser(): Promise { if (!this._user) { this._user = await this._fetchUser() } @@ -29,7 +30,7 @@ export class StateManager { return this._user } - private async _fetchApiKey() { + private async _fetchApiKey(): Promise> { const apiKeyResult = await this._api.apiKeys().current() if (!apiKeyResult.success) { throw new Error(`Failed to get API key: ${apiKeyResult.error.message}`) @@ -37,7 +38,7 @@ export class StateManager { return apiKeyResult.data } - async getApiKey() { + async getApiKey(): Promise> { let _apiKey = await this._cache.get('apiKey') if (!_apiKey) { @@ -48,7 +49,7 @@ export class StateManager { return _apiKey } - async getDistinctId() { + async getDistinctId(): Promise> { let _distinctId = await this._cache.get('distinctId') if (!_distinctId) { @@ -81,19 +82,13 @@ export class StateManager { return { projectId } } - if ( - scoped_organizations.length === 0 || - scoped_organizations.includes(activeOrganization.id) - ) { + if (scoped_organizations.length === 0 || scoped_organizations.includes(activeOrganization.id)) { return { organizationId: activeOrganization.id, projectId: activeTeam.id } } const organizationId = scoped_organizations[0]! - const projectsResult = await this._api - .organizations() - .projects({ orgId: organizationId }) - .list() + const projectsResult = await this._api.organizations().projects({ orgId: organizationId }).list() if (!projectsResult.success) { throw projectsResult.error @@ -108,7 +103,10 @@ export class StateManager { return { organizationId, projectId: Number(projectId) } } - async setDefaultOrganizationAndProject() { + async setDefaultOrganizationAndProject(): Promise<{ + organizationId: string | undefined + projectId: number + }> { const { organizationId, projectId } = await this._getDefaultOrganizationAndProject() if (organizationId) { diff --git a/products/mcp/typescript/src/lib/utils/api.ts b/products/mcp/typescript/src/lib/utils/api.ts index 6eafa96057..628c6f5e4d 100644 --- a/products/mcp/typescript/src/lib/utils/api.ts +++ b/products/mcp/typescript/src/lib/utils/api.ts @@ -1,12 +1,8 @@ -import { ApiListResponseSchema } from '@/schema/api' - import type { z } from 'zod' -export const withPagination = async ( - url: string, - apiToken: string, - dataSchema: z.ZodType -): Promise => { +import { ApiListResponseSchema } from '@/schema/api' + +export const withPagination = async (url: string, apiToken: string, dataSchema: z.ZodType): Promise => { const response = await fetch(url, { headers: { Authorization: `Bearer ${apiToken}`, @@ -33,22 +29,19 @@ export const withPagination = async ( return results } -export const hasScope = (scopes: string[], requiredScope: string) => { +export const hasScope = (scopes: string[], requiredScope: string): boolean => { if (scopes.includes('*')) { return true } // if read scoped required, and write present, return true - if ( - requiredScope.endsWith(':read') && - scopes.includes(requiredScope.replace(':read', ':write')) - ) { + if (requiredScope.endsWith(':read') && scopes.includes(requiredScope.replace(':read', ':write'))) { return true } return scopes.includes(requiredScope) } -export const hasScopes = (scopes: string[], requiredScopes: string[]) => { +export const hasScopes = (scopes: string[], requiredScopes: string[]): boolean => { return requiredScopes.every((scope) => hasScope(scopes, scope)) } diff --git a/products/mcp/typescript/src/lib/utils/helper-functions.ts b/products/mcp/typescript/src/lib/utils/helper-functions.ts index fa15911577..9e3a7d29c5 100644 --- a/products/mcp/typescript/src/lib/utils/helper-functions.ts +++ b/products/mcp/typescript/src/lib/utils/helper-functions.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto' -export function hash(data: string) { +export function hash(data: string): string { // Use PBKDF2 with sufficient computational effort for security // 100,000 iterations provides good security while maintaining reasonable performance const salt = crypto.createHash('sha256').update('posthog_mcp_salt').digest() diff --git a/products/mcp/typescript/src/prompts/index.ts b/products/mcp/typescript/src/prompts/index.ts index c2f83227d9..e31234d7fd 100644 --- a/products/mcp/typescript/src/prompts/index.ts +++ b/products/mcp/typescript/src/prompts/index.ts @@ -2,6 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { z } from 'zod' import type { Context } from '@/tools/types' + import { setupEventsPrompt } from './setup-events' export interface Prompt { @@ -27,7 +28,7 @@ export async function getPromptsFromContext(context: Context): Promise return [await setupEventsPrompt(context)] } -export async function registerPrompts(server: McpServer, context: Context) { +export async function registerPrompts(server: McpServer, context: Context): Promise { const prompts = await getPromptsFromContext(context) for (const prompt of prompts) { diff --git a/products/mcp/typescript/src/prompts/setup-events.ts b/products/mcp/typescript/src/prompts/setup-events.ts index 97df06de7a..1adc9abebd 100644 --- a/products/mcp/typescript/src/prompts/setup-events.ts +++ b/products/mcp/typescript/src/prompts/setup-events.ts @@ -1,14 +1,16 @@ -import type { Context } from '@/tools/types' -import type { Prompt } from './index' import { ResourceUri } from '@/resources' +import type { Context } from '@/tools/types' +import type { Prompt } from './index' + +// oxlint-disable-next-line @typescript-eslint/no-unused-vars export async function setupEventsPrompt(_context: Context): Promise { return { name: 'posthog-setup', title: 'Setup a deep PostHog integration', description: 'Automatically instrument your Next.js project with PostHog event tracking, user identification, and error tracking', - handler: async (_context, _args) => { + handler: async () => { return { messages: [ { diff --git a/products/mcp/typescript/src/resources/index.ts b/products/mcp/typescript/src/resources/index.ts index 8e7b51e455..0ee4b2610d 100644 --- a/products/mcp/typescript/src/resources/index.ts +++ b/products/mcp/typescript/src/resources/index.ts @@ -1,10 +1,12 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' + import type { Context } from '@/tools/types' + import { registerIntegrationResources } from './integration' // Re-export ResourceUri for external consumers export { ResourceUri } from './integration' -export function registerResources(server: McpServer, context: Context) { +export function registerResources(server: McpServer, context: Context): void { registerIntegrationResources(server, context) } diff --git a/products/mcp/typescript/src/resources/integration/index.ts b/products/mcp/typescript/src/resources/integration/index.ts index 6caf336c9f..d13df06696 100644 --- a/products/mcp/typescript/src/resources/integration/index.ts +++ b/products/mcp/typescript/src/resources/integration/index.ts @@ -1,15 +1,16 @@ import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' +import { type Unzipped, strFromU8, unzipSync } from 'fflate' + import type { Context } from '@/tools/types' + import { - FRAMEWORK_DOCS, EXAMPLES_MARKDOWN_URL, + FRAMEWORK_DOCS, FRAMEWORK_MARKDOWN_FILES, - isSupportedFramework, getSupportedFrameworks, getSupportedFrameworksList, + isSupportedFramework, } from './framework-mappings' -import { unzipSync, strFromU8, type Unzipped } from 'fflate' - // Import workflow markdown files import workflowBegin from './workflow-guides/1.0-event-setup-begin.md' import workflowEdit from './workflow-guides/1.1-event-setup-edit.md' @@ -35,8 +36,7 @@ export enum ResourceUri { /** * Message appended to workflow resources to indicate the next step */ -export const WORKFLOW_NEXT_STEP_MESSAGE = - 'Upon completion, access the following resource to continue:' +export const WORKFLOW_NEXT_STEP_MESSAGE = 'Upon completion, access the following resource to continue:' /** * URL to the identify() documentation @@ -67,7 +67,6 @@ async function fetchExamplesMarkdown(): Promise { return cachedExamplesMarkdown } - console.log('Fetching PostHog examples markdown...') const response = await fetch(EXAMPLES_MARKDOWN_URL) if (!response.ok) { @@ -77,7 +76,6 @@ async function fetchExamplesMarkdown(): Promise { const arrayBuffer = await response.arrayBuffer() const uint8Array = new Uint8Array(arrayBuffer) cachedExamplesMarkdown = unzipSync(uint8Array) - console.log('Examples markdown cached successfully') return cachedExamplesMarkdown } @@ -107,7 +105,8 @@ const workflowSequence = [ /** * Registers all PostHog integration resources with the MCP server */ -export function registerIntegrationResources(server: McpServer, _context: Context) { +// oxlint-disable-next-line @typescript-eslint/no-unused-vars +export function registerIntegrationResources(server: McpServer, _context: Context): void { // Fetch examples markdown at startup fetchExamplesMarkdown().catch((error) => { console.error('Failed to fetch examples markdown:', error) @@ -216,10 +215,7 @@ export function registerIntegrationResources(server: McpServer, _context: Contex const frameworks = getSupportedFrameworks() return { resources: frameworks.map((framework) => ({ - uri: ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace( - '{framework}', - framework - ), + uri: ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework), name: `PostHog ${framework} example project`, description: `Example project code for ${framework}`, mimeType: 'text/markdown', diff --git a/products/mcp/typescript/src/schema/api.ts b/products/mcp/typescript/src/schema/api.ts index a8c1624b1d..46e40eb2f7 100644 --- a/products/mcp/typescript/src/schema/api.ts +++ b/products/mcp/typescript/src/schema/api.ts @@ -35,6 +35,7 @@ export const ApiEventDefinitionSchema = z.object({ tags: z.array(z.string().nullish()).nullish(), }) +// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type export const ApiListResponseSchema = (dataSchema: T) => z.object({ count: z.number().nullish(), diff --git a/products/mcp/typescript/src/schema/experiments.ts b/products/mcp/typescript/src/schema/experiments.ts index d00461454e..b281d1eeda 100644 --- a/products/mcp/typescript/src/schema/experiments.ts +++ b/products/mcp/typescript/src/schema/experiments.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' + import { FeatureFlagSchema } from './flags' import { ExperimentCreateSchema as ToolExperimentCreateSchema, @@ -237,7 +238,7 @@ export type ExperimentUpdateApiPayload = z.infer { +const getPropertiesIfNotEmpty = (props: T): { properties?: T } => { return props && Object.keys(props).length > 0 ? { properties: props } : {} } @@ -365,67 +366,61 @@ export type ExperimentCreatePayload = z.output { - const updatePayload: Record = {} +export const ExperimentUpdateTransformSchema = ToolExperimentUpdateInputSchema.transform((input) => { + const updatePayload: Record = {} - // Basic fields - direct mapping - if (input.name !== undefined) { - updatePayload.name = input.name - } - if (input.description !== undefined) { - updatePayload.description = input.description - } - - // Transform metrics if provided - if (input.primary_metrics !== undefined) { - updatePayload.metrics = input.primary_metrics.map(transformMetricToApi) - updatePayload.primary_metrics_ordered_uuids = updatePayload.metrics.map( - (m: any) => m.uuid! - ) - } - - if (input.secondary_metrics !== undefined) { - updatePayload.metrics_secondary = input.secondary_metrics.map(transformMetricToApi) - updatePayload.secondary_metrics_ordered_uuids = updatePayload.metrics_secondary.map( - (m: any) => m.uuid! - ) - } - - // Transform minimum detectable effect into parameters - if (input.minimum_detectable_effect !== undefined) { - updatePayload.parameters = { - ...updatePayload.parameters, - minimum_detectable_effect: input.minimum_detectable_effect, - } - } - - // Handle experiment state management - if (input.launch === true) { - updatePayload.start_date = new Date().toISOString() - } - - if (input.conclude !== undefined) { - updatePayload.conclusion = input.conclude - updatePayload.end_date = new Date().toISOString() - if (input.conclusion_comment !== undefined) { - updatePayload.conclusion_comment = input.conclusion_comment - } - } - - if (input.restart === true) { - updatePayload.end_date = null - updatePayload.conclusion = null - updatePayload.conclusion_comment = null - } - - if (input.archive !== undefined) { - updatePayload.archived = input.archive - } - - return updatePayload + // Basic fields - direct mapping + if (input.name !== undefined) { + updatePayload.name = input.name } -).pipe(ExperimentUpdateApiPayloadSchema) + if (input.description !== undefined) { + updatePayload.description = input.description + } + + // Transform metrics if provided + if (input.primary_metrics !== undefined) { + updatePayload.metrics = input.primary_metrics.map(transformMetricToApi) + updatePayload.primary_metrics_ordered_uuids = updatePayload.metrics.map((m: any) => m.uuid!) + } + + if (input.secondary_metrics !== undefined) { + updatePayload.metrics_secondary = input.secondary_metrics.map(transformMetricToApi) + updatePayload.secondary_metrics_ordered_uuids = updatePayload.metrics_secondary.map((m: any) => m.uuid!) + } + + // Transform minimum detectable effect into parameters + if (input.minimum_detectable_effect !== undefined) { + updatePayload.parameters = { + ...updatePayload.parameters, + minimum_detectable_effect: input.minimum_detectable_effect, + } + } + + // Handle experiment state management + if (input.launch === true) { + updatePayload.start_date = new Date().toISOString() + } + + if (input.conclude !== undefined) { + updatePayload.conclusion = input.conclude + updatePayload.end_date = new Date().toISOString() + if (input.conclusion_comment !== undefined) { + updatePayload.conclusion_comment = input.conclusion_comment + } + } + + if (input.restart === true) { + updatePayload.end_date = null + updatePayload.conclusion = null + updatePayload.conclusion_comment = null + } + + if (input.archive !== undefined) { + updatePayload.archived = input.archive + } + + return updatePayload +}).pipe(ExperimentUpdateApiPayloadSchema) export type ExperimentUpdateTransform = z.output diff --git a/products/mcp/typescript/src/schema/flags.ts b/products/mcp/typescript/src/schema/flags.ts index e5e0916786..b5b0e6b4f1 100644 --- a/products/mcp/typescript/src/schema/flags.ts +++ b/products/mcp/typescript/src/schema/flags.ts @@ -10,36 +10,21 @@ export interface PostHogFlagsResponse { results?: PostHogFeatureFlag[] } const base = ['exact', 'is_not', 'is_set', 'is_not_set'] as const -const stringOps = [ - ...base, - 'icontains', - 'not_icontains', - 'regex', - 'not_regex', - 'is_cleaned_path_exact', -] as const +const stringOps = [...base, 'icontains', 'not_icontains', 'regex', 'not_regex', 'is_cleaned_path_exact'] as const const numberOps = [...base, 'gt', 'gte', 'lt', 'lte', 'min', 'max'] as const const booleanOps = [...base] as const const arrayOps = ['in', 'not_in'] as const -const operatorSchema = z.enum([ - ...stringOps, - ...numberOps, - ...booleanOps, - ...arrayOps, -] as unknown as [string, ...string[]]) +const operatorSchema = z.enum([...stringOps, ...numberOps, ...booleanOps, ...arrayOps] as unknown as [ + string, + ...string[], +]) export const PersonPropertyFilterSchema = z .object({ key: z.string(), - value: z.union([ - z.string(), - z.number(), - z.boolean(), - z.array(z.string()), - z.array(z.number()), - ]), + value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.array(z.number())]), operator: operatorSchema.optional(), }) .superRefine((data, ctx) => { diff --git a/products/mcp/typescript/src/schema/properties.ts b/products/mcp/typescript/src/schema/properties.ts index b639f45166..3e0c68cb9c 100644 --- a/products/mcp/typescript/src/schema/properties.ts +++ b/products/mcp/typescript/src/schema/properties.ts @@ -1,10 +1,7 @@ -import { - ApiEventDefinitionSchema, - type ApiListResponseSchema, - ApiPropertyDefinitionSchema, -} from '@/schema/api' import type { z } from 'zod' +import { ApiEventDefinitionSchema, type ApiListResponseSchema, ApiPropertyDefinitionSchema } from '@/schema/api' + export const PropertyDefinitionSchema = ApiPropertyDefinitionSchema.pick({ name: true, property_type: true, diff --git a/products/mcp/typescript/src/schema/query.ts b/products/mcp/typescript/src/schema/query.ts index 2568dd95d0..a456b55947 100644 --- a/products/mcp/typescript/src/schema/query.ts +++ b/products/mcp/typescript/src/schema/query.ts @@ -198,10 +198,7 @@ const DataVisualizationNodeSchema = z.object({ }) // Any insight query -const InsightQuerySchema = z.discriminatedUnion('kind', [ - InsightVizNodeSchema, - DataVisualizationNodeSchema, -]) +const InsightQuerySchema = z.discriminatedUnion('kind', [InsightVizNodeSchema, DataVisualizationNodeSchema]) // Export all schemas export { diff --git a/products/mcp/typescript/src/schema/surveys.ts b/products/mcp/typescript/src/schema/surveys.ts index 9c60cfb8d3..02a81907c9 100644 --- a/products/mcp/typescript/src/schema/surveys.ts +++ b/products/mcp/typescript/src/schema/surveys.ts @@ -1,4 +1,5 @@ import { z } from 'zod' + import { FilterGroupsSchema } from './flags.js' // Survey question types @@ -29,9 +30,7 @@ const ChoiceResponseBranching = z "Only include keys for responses that should branch to a specific question or 'end'. Omit keys for responses that should proceed to the next question (default behavior)." ), }) - .describe( - 'For single choice questions: use choice indices as string keys ("0", "1", "2", etc.)' - ) + .describe('For single choice questions: use choice indices as string keys ("0", "1", "2", etc.)') // NPS sentiment branching - uses sentiment categories const NPSSentimentBranching = z @@ -41,9 +40,7 @@ const NPSSentimentBranching = z .record( z .enum(['detractors', 'passives', 'promoters']) - .describe( - 'NPS sentiment categories: detractors (0-6), passives (7-8), promoters (9-10)' - ), + .describe('NPS sentiment categories: detractors (0-6), passives (7-8), promoters (9-10)'), z.union([z.number(), z.literal('end')]) ) .describe( @@ -95,12 +92,7 @@ const ChoiceBranching = z.union([ SpecificQuestionBranching, ]) -const NPSBranching = z.union([ - NextQuestionBranching, - EndBranching, - NPSSentimentBranching, - SpecificQuestionBranching, -]) +const NPSBranching = z.union([NextQuestionBranching, EndBranching, NPSSentimentBranching, SpecificQuestionBranching]) const RatingBranching = z.union([ NextQuestionBranching, @@ -129,14 +121,8 @@ const RatingQuestion = BaseSurveyQuestionSchema.extend({ .union([z.literal(3), z.literal(5), z.literal(7)]) .optional() .describe('Rating scale can be one of 3, 5, or 7'), - lowerBoundLabel: z - .string() - .optional() - .describe("Label for the lowest rating (e.g., 'Very Poor')"), - upperBoundLabel: z - .string() - .optional() - .describe("Label for the highest rating (e.g., 'Excellent')"), + lowerBoundLabel: z.string().optional().describe("Label for the lowest rating (e.g., 'Very Poor')"), + upperBoundLabel: z.string().optional().describe("Label for the highest rating (e.g., 'Excellent')"), branching: RatingBranching.optional(), }).superRefine((data, ctx) => { // Validate display-specific scale constraints @@ -178,14 +164,8 @@ const NPSRatingQuestion = BaseSurveyQuestionSchema.extend({ type: z.literal('rating'), display: z.literal('number').describe('NPS questions always use numeric scale'), scale: z.literal(10).describe('NPS questions always use 0-10 scale'), - lowerBoundLabel: z - .string() - .optional() - .describe("Label for 0 rating (typically 'Not at all likely')"), - upperBoundLabel: z - .string() - .optional() - .describe("Label for 10 rating (typically 'Extremely likely')"), + lowerBoundLabel: z.string().optional().describe("Label for 0 rating (typically 'Not at all likely')"), + upperBoundLabel: z.string().optional().describe("Label for 10 rating (typically 'Extremely likely')"), branching: NPSBranching.optional(), }).superRefine((data, ctx) => { // Validate response-based branching for NPS rating questions @@ -212,13 +192,8 @@ const SingleChoiceQuestion = BaseSurveyQuestionSchema.extend({ .array(z.string().min(1, 'Choice text cannot be empty')) .min(2, 'Must have at least 2 choices') .max(20, 'Cannot have more than 20 choices') - .describe( - 'Array of choice options. Choice indices (0, 1, 2, etc.) are used for branching logic' - ), - shuffleOptions: z - .boolean() - .optional() - .describe('Whether to randomize the order of choices for each respondent'), + .describe('Array of choice options. Choice indices (0, 1, 2, etc.) are used for branching logic'), + shuffleOptions: z.boolean().optional().describe('Whether to randomize the order of choices for each respondent'), hasOpenChoice: z .boolean() .optional() @@ -269,13 +244,8 @@ const MultipleChoiceQuestion = BaseSurveyQuestionSchema.extend({ .array(z.string().min(1, 'Choice text cannot be empty')) .min(2, 'Must have at least 2 choices') .max(20, 'Cannot have more than 20 choices') - .describe( - 'Array of choice options. Multiple selections allowed. No branching logic supported.' - ), - shuffleOptions: z - .boolean() - .optional() - .describe('Whether to randomize the order of choices for each respondent'), + .describe('Array of choice options. Multiple selections allowed. No branching logic supported.'), + shuffleOptions: z.boolean().optional().describe('Whether to randomize the order of choices for each respondent'), hasOpenChoice: z .boolean() .optional() @@ -360,10 +330,7 @@ const SurveyConditions = z.object({ .optional(), deviceTypes: z.array(z.enum(['Desktop', 'Mobile', 'Tablet'])).optional(), deviceTypesMatchType: MatchTypeEnum.optional(), - linkedFlagVariant: z - .string() - .optional() - .describe('The variant of the feature flag linked to this survey'), + linkedFlagVariant: z.string().optional().describe('The variant of the feature flag linked to this survey'), }) // Survey appearance customization - input schema @@ -453,11 +420,7 @@ export const CreateSurveyInputSchema = z.object({ .describe( 'When at least one question is answered, the response is stored (true). The response is stored when all questions are answered (false).' ), - linked_flag_id: z - .number() - .nullable() - .optional() - .describe('The feature flag linked to this survey'), + linked_flag_id: z.number().nullable().optional().describe('The feature flag linked to this survey'), targeting_flag_filters: FilterGroupsSchema.optional().describe( "Target specific users based on their properties. Example: {groups: [{properties: [{key: 'email', value: ['@company.com'], operator: 'icontains'}], rollout_percentage: 100}]}" ), @@ -467,10 +430,7 @@ export const UpdateSurveyInputSchema = z.object({ name: z.string().min(1, 'Survey name cannot be empty').optional(), description: z.string().optional(), type: z.enum(['popover', 'api', 'widget', 'external_survey']).optional(), - questions: z - .array(SurveyQuestionInputSchema) - .min(1, 'Survey must have at least one question') - .optional(), + questions: z.array(SurveyQuestionInputSchema).min(1, 'Survey must have at least one question').optional(), conditions: SurveyConditions.optional(), appearance: SurveyAppearance.optional(), schedule: z @@ -483,16 +443,12 @@ export const UpdateSurveyInputSchema = z.object({ .string() .datetime() .optional() - .describe( - 'When the survey should start being shown to users. Setting this will launch the survey' - ), + .describe('When the survey should start being shown to users. Setting this will launch the survey'), end_date: z .string() .datetime() .optional() - .describe( - 'When the survey stopped being shown to users. Setting this will complete the survey.' - ), + .describe('When the survey stopped being shown to users. Setting this will complete the survey.'), archived: z.boolean().optional(), responses_limit: z .number() @@ -523,15 +479,8 @@ export const UpdateSurveyInputSchema = z.object({ .describe( 'When at least one question is answered, the response is stored (true). The response is stored when all questions are answered (false).' ), - linked_flag_id: z - .number() - .nullable() - .optional() - .describe('The feature flag to link to this survey'), - targeting_flag_id: z - .number() - .optional() - .describe('An existing targeting flag to use for this survey'), + linked_flag_id: z.number().nullable().optional().describe('The feature flag to link to this survey'), + targeting_flag_id: z.number().optional().describe('An existing targeting flag to use for this survey'), targeting_flag_filters: FilterGroupsSchema.optional().describe( "Target specific users based on their properties. Example: {groups: [{properties: [{key: 'email', value: ['@company.com'], operator: 'icontains'}], rollout_percentage: 50}]}" ), diff --git a/products/mcp/typescript/src/schema/tool-inputs.ts b/products/mcp/typescript/src/schema/tool-inputs.ts index 9efbef0f79..e378a89c08 100644 --- a/products/mcp/typescript/src/schema/tool-inputs.ts +++ b/products/mcp/typescript/src/schema/tool-inputs.ts @@ -1,4 +1,5 @@ import { z } from 'zod' + import { AddInsightToDashboardSchema, CreateDashboardInputSchema, @@ -84,9 +85,7 @@ export const ExperimentUpdateInputSchema = z.object({ .describe( "Metric type: 'mean' for average values, 'funnel' for conversion flows, 'ratio' for comparing two metrics" ), - event_name: z - .string() - .describe("PostHog event name (e.g., '$pageview', 'add_to_cart', 'purchase')"), + event_name: z.string().describe("PostHog event name (e.g., '$pageview', 'add_to_cart', 'purchase')"), funnel_steps: z .array(z.string()) .optional() @@ -104,10 +103,7 @@ export const ExperimentUpdateInputSchema = z.object({ name: z.string().optional().describe('Human-readable metric name'), metric_type: z.enum(['mean', 'funnel', 'ratio']).describe('Metric type'), event_name: z.string().describe('PostHog event name'), - funnel_steps: z - .array(z.string()) - .optional() - .describe('For funnel metrics only: Array of event names'), + funnel_steps: z.array(z.string()).optional().describe('For funnel metrics only: Array of event names'), properties: z.record(z.any()).optional().describe('Event properties to filter on'), description: z.string().optional().describe('What this metric measures'), }) @@ -115,10 +111,7 @@ export const ExperimentUpdateInputSchema = z.object({ .optional() .describe('Update secondary metrics'), - minimum_detectable_effect: z - .number() - .optional() - .describe('Update minimum detectable effect in percentage'), + minimum_detectable_effect: z.number().optional().describe('Update minimum detectable effect in percentage'), // Experiment state management launch: z.boolean().optional().describe('Launch experiment (set start_date) or keep as draft'), @@ -130,26 +123,18 @@ export const ExperimentUpdateInputSchema = z.object({ conclusion_comment: z.string().optional().describe('Comment about experiment conclusion'), - restart: z - .boolean() - .optional() - .describe('Restart concluded experiment (clears end_date and conclusion)'), + restart: z.boolean().optional().describe('Restart concluded experiment (clears end_date and conclusion)'), archive: z.boolean().optional().describe('Archive or unarchive experiment'), }) export const ExperimentUpdateSchema = z.object({ experimentId: z.number().describe('The ID of the experiment to update'), - data: ExperimentUpdateInputSchema.describe( - 'The experiment data to update using user-friendly format' - ), + data: ExperimentUpdateInputSchema.describe('The experiment data to update using user-friendly format'), }) export const ExperimentCreateSchema = z.object({ - name: z - .string() - .min(1) - .describe('Experiment name - should clearly describe what is being tested'), + name: z.string().min(1).describe('Experiment name - should clearly describe what is being tested'), description: z .string() @@ -167,9 +152,7 @@ export const ExperimentCreateSchema = z.object({ type: z .enum(['product', 'web']) .default('product') - .describe( - "Experiment type: 'product' for backend/API changes, 'web' for frontend UI changes" - ), + .describe("Experiment type: 'product' for backend/API changes, 'web' for frontend UI changes"), // Primary metrics with guidance primary_metrics: z @@ -196,9 +179,7 @@ export const ExperimentCreateSchema = z.object({ description: z .string() .optional() - .describe( - "What this metric measures and why it's important for the experiment" - ), + .describe("What this metric measures and why it's important for the experiment"), }) ) .optional() @@ -216,9 +197,7 @@ export const ExperimentCreateSchema = z.object({ .describe( "Metric type: 'mean' for average values, 'funnel' for conversion flows, 'ratio' for comparing two metrics" ), - event_name: z - .string() - .describe("REQUIRED: PostHog event name. Use '$pageview' if unsure."), + event_name: z.string().describe("REQUIRED: PostHog event name. Use '$pageview' if unsure."), funnel_steps: z .array(z.string()) .optional() @@ -236,15 +215,9 @@ export const ExperimentCreateSchema = z.object({ variants: z .array( z.object({ - key: z - .string() - .describe("Variant key (e.g., 'control', 'variant_a', 'new_design')"), + key: z.string().describe("Variant key (e.g., 'control', 'variant_a', 'new_design')"), name: z.string().optional().describe('Human-readable variant name'), - rollout_percentage: z - .number() - .min(0) - .max(100) - .describe('Percentage of users to show this variant'), + rollout_percentage: z.number().min(0).max(100).describe('Percentage of users to show this variant'), }) ) .optional() @@ -261,10 +234,7 @@ export const ExperimentCreateSchema = z.object({ ), // Exposure and targeting - filter_test_accounts: z - .boolean() - .default(true) - .describe('Whether to filter out internal test accounts'), + filter_test_accounts: z.boolean().default(true).describe('Whether to filter out internal test accounts'), target_properties: z .record(z.any()) @@ -275,16 +245,12 @@ export const ExperimentCreateSchema = z.object({ draft: z .boolean() .default(true) - .describe( - 'Create as draft (true) or launch immediately (false). Recommend draft for review first' - ), + .describe('Create as draft (true) or launch immediately (false). Recommend draft for review first'), holdout_id: z .number() .optional() - .describe( - 'Holdout group ID if this experiment should exclude users from other experiments' - ), + .describe('Holdout group ID if this experiment should exclude users from other experiments'), }) export const FeatureFlagCreateSchema = z.object({ @@ -360,22 +326,13 @@ export const OrganizationSetActiveSchema = z.object({ export const ProjectGetAllSchema = z.object({}) export const ProjectEventDefinitionsSchema = z.object({ - q: z - .string() - .optional() - .describe('Search query to filter event names. Only use if there are lots of events.'), + q: z.string().optional().describe('Search query to filter event names. Only use if there are lots of events.'), }) export const ProjectPropertyDefinitionsInputSchema = z.object({ type: z.enum(['event', 'person']).describe('Type of properties to get'), - eventName: z - .string() - .describe('Event name to filter properties by, required for event type') - .optional(), - includePredefinedProperties: z - .boolean() - .optional() - .describe('Whether to include predefined properties'), + eventName: z.string().describe('Event name to filter properties by, required for event type').optional(), + includePredefinedProperties: z.boolean().optional().describe('Whether to include predefined properties'), }) export const ProjectSetActiveSchema = z.object({ diff --git a/products/mcp/typescript/src/tools/README.md b/products/mcp/typescript/src/tools/README.md index ba2f9f21ce..eba29f1150 100644 --- a/products/mcp/typescript/src/tools/README.md +++ b/products/mcp/typescript/src/tools/README.md @@ -22,12 +22,12 @@ Define your tool's input schema using Zod. Keep inputs **simple and user-friendl ```typescript export const FeatureFlagCreateSchema = z.object({ - name: z.string(), - key: z.string(), - description: z.string(), - filters: FilterGroupsSchema, - active: z.boolean(), - tags: z.array(z.string()).optional(), + name: z.string(), + key: z.string(), + description: z.string(), + filters: FilterGroupsSchema, + active: z.boolean(), + tags: z.array(z.string()).optional(), }) ``` @@ -41,41 +41,42 @@ export const FeatureFlagCreateSchema = z.object({ ### 2. Create Tool Handler (`tools/featureFlags/create.ts`) ```typescript +import type { z } from 'zod' + import { FeatureFlagCreateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagCreateSchema type Params = z.infer export const createHandler = async (context: Context, params: Params) => { - const { name, key, description, filters, active, tags } = params - const projectId = await context.stateManager.getProjectId() + const { name, key, description, filters, active, tags } = params + const projectId = await context.stateManager.getProjectId() - // Call API client method - const flagResult = await context.api.featureFlags({ projectId }).create({ - data: { name, key, description, filters, active, tags }, - }) + // Call API client method + const flagResult = await context.api.featureFlags({ projectId }).create({ + data: { name, key, description, filters, active, tags }, + }) - if (!flagResult.success) { - throw new Error(`Failed to create feature flag: ${flagResult.error.message}`) - } + if (!flagResult.success) { + throw new Error(`Failed to create feature flag: ${flagResult.error.message}`) + } - // Add context that is useful, like in this case a URL for the LLM to link to. - const featureFlagWithUrl = { - ...flagResult.data, - url: `${context.api.getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, - } + // Add context that is useful, like in this case a URL for the LLM to link to. + const featureFlagWithUrl = { + ...flagResult.data, + url: `${context.api.getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, + } - return { - content: [{ type: 'text', text: JSON.stringify(featureFlagWithUrl) }], - } + return { + content: [{ type: 'text', text: JSON.stringify(featureFlagWithUrl) }], + } } const tool = (): ToolBase => ({ - name: 'create-feature-flag', - schema, - handler: createHandler, + name: 'create-feature-flag', + schema, + handler: createHandler, }) export default tool @@ -95,20 +96,20 @@ Add a clear, actionable description for your tool, assign it to a feature, speci ```json { - "create-feature-flag": { - "title": "Create Feature Flag", - "description": "Creates a new feature flag in the project. Once you have created a feature flag, you should: Ask the user if they want to add it to their codebase, Use the \"search-docs\" tool to find documentation on how to add feature flags to the codebase (search for the right language / framework), Clarify where it should be added and then add it.", - "category": "Feature flags", // This will be displayed in the docs, but not readable by the MCP client - "feature": "flags", - "summary": "Creates a new feature flag in the project.", // This will be displayed in the docs, but not readable by the MCP client. - "required_scopes": ["feature_flag:write"], // You can find a list of available scopes here: https://github.com/PostHog/posthog/blob/31082f4bcc4c45a0ac830777b8a3048e7752a1bc/frontend/src/lib/scopes.tsx - "annotations": { - "destructiveHint": false, // Does the tool delete or destructively modify data? - "idempotentHint": false, // Can the tool be safely called multiple times with same result? - "openWorldHint": true, // Does the tool interact with external systems or create new resources? - "readOnlyHint": false // Is the tool read-only (doesn't modify any state)? - } + "create-feature-flag": { + "title": "Create Feature Flag", + "description": "Creates a new feature flag in the project. Once you have created a feature flag, you should: Ask the user if they want to add it to their codebase, Use the \"search-docs\" tool to find documentation on how to add feature flags to the codebase (search for the right language / framework), Clarify where it should be added and then add it.", + "category": "Feature flags", // This will be displayed in the docs, but not readable by the MCP client + "feature": "flags", + "summary": "Creates a new feature flag in the project.", // This will be displayed in the docs, but not readable by the MCP client. + "required_scopes": ["feature_flag:write"], // You can find a list of available scopes here: https://github.com/PostHog/posthog/blob/31082f4bcc4c45a0ac830777b8a3048e7752a1bc/frontend/src/lib/scopes.tsx + "annotations": { + "destructiveHint": false, // Does the tool delete or destructively modify data? + "idempotentHint": false, // Can the tool be safely called multiple times with same result? + "openWorldHint": true, // Does the tool interact with external systems or create new resources? + "readOnlyHint": false // Is the tool read-only (doesn't modify any state)? } + } } ``` @@ -140,63 +141,64 @@ If you do add a new feature, make sure to update the `README.md` in the root of Always include integration tests to help us catch if there is a change to the underlying API: ```typescript -import { - cleanupResources, - createTestClient, - createTestContext, - generateUniqueKey, - parseToolResponse, - setActiveProjectAndOrg, -} from '@/shared/test-utils' -import createFeatureFlagTool from '@/tools/featureFlags/create' import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + cleanupResources, + createTestClient, + createTestContext, + generateUniqueKey, + parseToolResponse, + setActiveProjectAndOrg, +} from '@/shared/test-utils' +import createFeatureFlagTool from '@/tools/featureFlags/create' + describe('Feature Flags', () => { - let context: Context - const createdResources: CreatedResources = { - featureFlags: [], - insights: [], - dashboards: [], - } + let context: Context + const createdResources: CreatedResources = { + featureFlags: [], + insights: [], + dashboards: [], + } - beforeAll(async () => { - const client = createTestClient() - context = createTestContext(client) - await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!) + beforeAll(async () => { + const client = createTestClient() + context = createTestContext(client) + await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!) + }) + + afterEach(async () => { + await cleanupResources(context.api, TEST_PROJECT_ID!, createdResources) + }) + + describe('create-feature-flag tool', () => { + const createTool = createFeatureFlagTool() + + it('should create a feature flag with minimal required fields', async () => { + const params = { + name: 'Test Feature Flag', + key: generateUniqueKey('test-flag'), + description: 'Integration test flag', + filters: { groups: [] }, + active: true, + } + + const result = await createTool.handler(context, params) + const flagData = parseToolResponse(result) + + expect(flagData.id).toBeTruthy() + expect(flagData.key).toBe(params.key) + expect(flagData.name).toBe(params.name) + expect(flagData.active).toBe(params.active) + expect(flagData.url).toContain('/feature_flags/') + + createdResources.featureFlags.push(flagData.id) }) - afterEach(async () => { - await cleanupResources(context.api, TEST_PROJECT_ID!, createdResources) - }) - - describe('create-feature-flag tool', () => { - const createTool = createFeatureFlagTool() - - it('should create a feature flag with minimal required fields', async () => { - const params = { - name: 'Test Feature Flag', - key: generateUniqueKey('test-flag'), - description: 'Integration test flag', - filters: { groups: [] }, - active: true, - } - - const result = await createTool.handler(context, params) - const flagData = parseToolResponse(result) - - expect(flagData.id).toBeDefined() - expect(flagData.key).toBe(params.key) - expect(flagData.name).toBe(params.name) - expect(flagData.active).toBe(params.active) - expect(flagData.url).toContain('/feature_flags/') - - createdResources.featureFlags.push(flagData.id) - }) - - it('should create a feature flag with complex filters', async () => { - // Test with more complex scenarios - }) + it('should create a feature flag with complex filters', async () => { + // Test with more complex scenarios }) + }) }) ``` diff --git a/products/mcp/typescript/src/tools/dashboards/addInsight.ts b/products/mcp/typescript/src/tools/dashboards/addInsight.ts index a99333f2b5..6bb05726d9 100644 --- a/products/mcp/typescript/src/tools/dashboards/addInsight.ts +++ b/products/mcp/typescript/src/tools/dashboards/addInsight.ts @@ -1,21 +1,20 @@ +import type { z } from 'zod' + import { DashboardAddInsightSchema } from '@/schema/tool-inputs' import { resolveInsightId } from '@/tools/insights/utils' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardAddInsightSchema type Params = z.infer -export const addInsightHandler = async (context: Context, params: Params) => { +export const addInsightHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { data } = params const projectId = await context.stateManager.getProjectId() const numericInsightId = await resolveInsightId(context, data.insightId, projectId) - const insightResult = await context.api - .insights({ projectId }) - .get({ insightId: data.insightId }) + const insightResult = await context.api.insights({ projectId }).get({ insightId: data.insightId }) if (!insightResult.success) { throw new Error(`Failed to get insight: ${insightResult.error.message}`) diff --git a/products/mcp/typescript/src/tools/dashboards/create.ts b/products/mcp/typescript/src/tools/dashboards/create.ts index 8c83a65556..010fb60dbc 100644 --- a/products/mcp/typescript/src/tools/dashboards/create.ts +++ b/products/mcp/typescript/src/tools/dashboards/create.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { DashboardCreateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardCreateSchema type Params = z.infer -export const createHandler = async (context: Context, params: Params) => { +export const createHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { data } = params const projectId = await context.stateManager.getProjectId() const dashboardResult = await context.api.dashboards({ projectId }).create({ data }) diff --git a/products/mcp/typescript/src/tools/dashboards/delete.ts b/products/mcp/typescript/src/tools/dashboards/delete.ts index 4fab5cdcab..a5ade445a7 100644 --- a/products/mcp/typescript/src/tools/dashboards/delete.ts +++ b/products/mcp/typescript/src/tools/dashboards/delete.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { DashboardDeleteSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardDeleteSchema type Params = z.infer -export const deleteHandler = async (context: Context, params: Params) => { +export const deleteHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { dashboardId } = params const projectId = await context.stateManager.getProjectId() const result = await context.api.dashboards({ projectId }).delete({ dashboardId }) diff --git a/products/mcp/typescript/src/tools/dashboards/get.ts b/products/mcp/typescript/src/tools/dashboards/get.ts index afe14cbdee..d03ad102ae 100644 --- a/products/mcp/typescript/src/tools/dashboards/get.ts +++ b/products/mcp/typescript/src/tools/dashboards/get.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { DashboardGetSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardGetSchema type Params = z.infer -export const getHandler = async (context: Context, params: Params) => { +export const getHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { dashboardId } = params const projectId = await context.stateManager.getProjectId() const dashboardResult = await context.api.dashboards({ projectId }).get({ dashboardId }) diff --git a/products/mcp/typescript/src/tools/dashboards/getAll.ts b/products/mcp/typescript/src/tools/dashboards/getAll.ts index b21aff5d8e..460dd54ab9 100644 --- a/products/mcp/typescript/src/tools/dashboards/getAll.ts +++ b/products/mcp/typescript/src/tools/dashboards/getAll.ts @@ -1,17 +1,16 @@ +import type { z } from 'zod' + import { DashboardGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardGetAllSchema type Params = z.infer -export const getAllHandler = async (context: Context, params: Params) => { +export const getAllHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { data } = params const projectId = await context.stateManager.getProjectId() - const dashboardsResult = await context.api - .dashboards({ projectId }) - .list({ params: data ?? {} }) + const dashboardsResult = await context.api.dashboards({ projectId }).list({ params: data ?? {} }) if (!dashboardsResult.success) { throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`) diff --git a/products/mcp/typescript/src/tools/dashboards/update.ts b/products/mcp/typescript/src/tools/dashboards/update.ts index 0e8ee6fd17..a92e5d8256 100644 --- a/products/mcp/typescript/src/tools/dashboards/update.ts +++ b/products/mcp/typescript/src/tools/dashboards/update.ts @@ -1,17 +1,16 @@ +import type { z } from 'zod' + import { DashboardUpdateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DashboardUpdateSchema type Params = z.infer -export const updateHandler = async (context: Context, params: Params) => { +export const updateHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { dashboardId, data } = params const projectId = await context.stateManager.getProjectId() - const dashboardResult = await context.api - .dashboards({ projectId }) - .update({ dashboardId, data }) + const dashboardResult = await context.api.dashboards({ projectId }).update({ dashboardId, data }) if (!dashboardResult.success) { throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`) diff --git a/products/mcp/typescript/src/tools/documentation/searchDocs.ts b/products/mcp/typescript/src/tools/documentation/searchDocs.ts index 434b6b73a0..728689d6dc 100644 --- a/products/mcp/typescript/src/tools/documentation/searchDocs.ts +++ b/products/mcp/typescript/src/tools/documentation/searchDocs.ts @@ -1,13 +1,14 @@ +import type { z } from 'zod' + import { docsSearch } from '@/inkeepApi' import { DocumentationSearchSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = DocumentationSearchSchema type Params = z.infer -export const searchDocsHandler = async (context: Context, params: Params) => { +export const searchDocsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { query } = params const inkeepApiKey = context.env.INKEEP_API_KEY diff --git a/products/mcp/typescript/src/tools/errorTracking/errorDetails.ts b/products/mcp/typescript/src/tools/errorTracking/errorDetails.ts index 9dd0d073d6..3c4a215009 100644 --- a/products/mcp/typescript/src/tools/errorTracking/errorDetails.ts +++ b/products/mcp/typescript/src/tools/errorTracking/errorDetails.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { ErrorTrackingDetailsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ErrorTrackingDetailsSchema type Params = z.infer -export const errorDetailsHandler = async (context: Context, params: Params) => { +export const errorDetailsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { issueId, dateFrom, dateTo } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/errorTracking/listErrors.ts b/products/mcp/typescript/src/tools/errorTracking/listErrors.ts index 9437ecca27..40764e39e8 100644 --- a/products/mcp/typescript/src/tools/errorTracking/listErrors.ts +++ b/products/mcp/typescript/src/tools/errorTracking/listErrors.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { ErrorTrackingListSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ErrorTrackingListSchema type Params = z.infer -export const listErrorsHandler = async (context: Context, params: Params) => { +export const listErrorsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { orderBy, dateFrom, dateTo, orderDirection, filterTestAccounts, status } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/experiments/create.ts b/products/mcp/typescript/src/tools/experiments/create.ts index 1f6c740bac..c93f0fa566 100644 --- a/products/mcp/typescript/src/tools/experiments/create.ts +++ b/products/mcp/typescript/src/tools/experiments/create.ts @@ -1,6 +1,7 @@ +import type { z } from 'zod' + import { ExperimentCreateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ExperimentCreateSchema @@ -10,7 +11,7 @@ type Params = z.infer * Create a comprehensive A/B test experiment with guided setup * This tool helps users create well-configured experiments through conversation */ -export const createExperimentHandler = async (context: Context, params: Params) => { +export const createExperimentHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() const result = await context.api.experiments({ projectId }).create(params) diff --git a/products/mcp/typescript/src/tools/experiments/delete.ts b/products/mcp/typescript/src/tools/experiments/delete.ts index c6482e7a88..7e71e1b5b5 100644 --- a/products/mcp/typescript/src/tools/experiments/delete.ts +++ b/products/mcp/typescript/src/tools/experiments/delete.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { ExperimentDeleteSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ExperimentDeleteSchema type Params = z.infer -export const deleteHandler = async (context: Context, { experimentId }: Params) => { +export const deleteHandler: ToolBase['handler'] = async (context: Context, { experimentId }: Params) => { const projectId = await context.stateManager.getProjectId() const deleteResult = await context.api.experiments({ projectId }).delete({ diff --git a/products/mcp/typescript/src/tools/experiments/get.ts b/products/mcp/typescript/src/tools/experiments/get.ts index 09ac27c4e0..8a12d11d49 100644 --- a/products/mcp/typescript/src/tools/experiments/get.ts +++ b/products/mcp/typescript/src/tools/experiments/get.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { ExperimentGetSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ExperimentGetSchema type Params = z.infer -export const getHandler = async (context: Context, { experimentId }: Params) => { +export const getHandler: ToolBase['handler'] = async (context: Context, { experimentId }: Params) => { const projectId = await context.stateManager.getProjectId() const result = await context.api.experiments({ projectId }).get({ diff --git a/products/mcp/typescript/src/tools/experiments/getAll.ts b/products/mcp/typescript/src/tools/experiments/getAll.ts index ee2b82dc31..c58cdf2829 100644 --- a/products/mcp/typescript/src/tools/experiments/getAll.ts +++ b/products/mcp/typescript/src/tools/experiments/getAll.ts @@ -1,12 +1,9 @@ import { ExperimentGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ExperimentGetAllSchema -type Params = z.infer - -export const getAllHandler = async (context: Context, _params: Params) => { +export const getAllHandler: ToolBase['handler'] = async (context: Context) => { const projectId = await context.stateManager.getProjectId() const results = await context.api.experiments({ projectId }).list() diff --git a/products/mcp/typescript/src/tools/experiments/getResults.ts b/products/mcp/typescript/src/tools/experiments/getResults.ts index ec3474b322..9d69fa41e1 100644 --- a/products/mcp/typescript/src/tools/experiments/getResults.ts +++ b/products/mcp/typescript/src/tools/experiments/getResults.ts @@ -1,7 +1,8 @@ +import type { z } from 'zod' + import { ExperimentResultsResponseSchema } from '@/schema/experiments' import { ExperimentResultsGetSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ExperimentResultsGetSchema @@ -12,7 +13,7 @@ type Params = z.infer * This tool fetches the experiment details and executes the necessary queries * to get metrics results (both primary and secondary) and exposure data */ -export const getResultsHandler = async (context: Context, params: Params) => { +export const getResultsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() const result = await context.api.experiments({ projectId }).getMetricResults({ diff --git a/products/mcp/typescript/src/tools/experiments/update.ts b/products/mcp/typescript/src/tools/experiments/update.ts index 852ab221c0..a028404323 100644 --- a/products/mcp/typescript/src/tools/experiments/update.ts +++ b/products/mcp/typescript/src/tools/experiments/update.ts @@ -1,14 +1,15 @@ +import type { z } from 'zod' + import { ExperimentUpdateTransformSchema } from '@/schema/experiments' import { ExperimentUpdateSchema } from '@/schema/tool-inputs' import { getToolDefinition } from '@/tools/toolDefinitions' -import type { Context, Tool } from '@/tools/types' -import type { z } from 'zod' +import type { Context, Tool, ToolBase } from '@/tools/types' const schema = ExperimentUpdateSchema type Params = z.infer -export const updateHandler = async (context: Context, params: Params) => { +export const updateHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { experimentId, data } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/featureFlags/create.ts b/products/mcp/typescript/src/tools/featureFlags/create.ts index ae8dac6ce2..0e767ea0e4 100644 --- a/products/mcp/typescript/src/tools/featureFlags/create.ts +++ b/products/mcp/typescript/src/tools/featureFlags/create.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { FeatureFlagCreateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagCreateSchema type Params = z.infer -export const createHandler = async (context: Context, params: Params) => { +export const createHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { name, key, description, filters, active, tags } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/featureFlags/delete.ts b/products/mcp/typescript/src/tools/featureFlags/delete.ts index 7774a805f0..66bcf47692 100644 --- a/products/mcp/typescript/src/tools/featureFlags/delete.ts +++ b/products/mcp/typescript/src/tools/featureFlags/delete.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { FeatureFlagDeleteSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagDeleteSchema type Params = z.infer -export const deleteHandler = async (context: Context, params: Params) => { +export const deleteHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { flagKey } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/featureFlags/getAll.ts b/products/mcp/typescript/src/tools/featureFlags/getAll.ts index b74e0376e8..29eddd0e2f 100644 --- a/products/mcp/typescript/src/tools/featureFlags/getAll.ts +++ b/products/mcp/typescript/src/tools/featureFlags/getAll.ts @@ -1,12 +1,9 @@ import { FeatureFlagGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagGetAllSchema -type Params = z.infer - -export const getAllHandler = async (context: Context, _params: Params) => { +export const getAllHandler: ToolBase['handler'] = async (context: Context) => { const projectId = await context.stateManager.getProjectId() const flagsResult = await context.api.featureFlags({ projectId }).list() diff --git a/products/mcp/typescript/src/tools/featureFlags/getDefinition.ts b/products/mcp/typescript/src/tools/featureFlags/getDefinition.ts index 5ba4323edf..c5c7085ad0 100644 --- a/products/mcp/typescript/src/tools/featureFlags/getDefinition.ts +++ b/products/mcp/typescript/src/tools/featureFlags/getDefinition.ts @@ -1,12 +1,16 @@ +import type { z } from 'zod' + import { FeatureFlagGetDefinitionSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagGetDefinitionSchema type Params = z.infer -export const getDefinitionHandler = async (context: Context, { flagId, flagKey }: Params) => { +export const getDefinitionHandler: ToolBase['handler'] = async ( + context: Context, + { flagId, flagKey }: Params +) => { if (!flagId && !flagKey) { return { content: [ @@ -21,9 +25,7 @@ export const getDefinitionHandler = async (context: Context, { flagId, flagKey } const projectId = await context.stateManager.getProjectId() if (flagId) { - const flagResult = await context.api - .featureFlags({ projectId }) - .get({ flagId: String(flagId) }) + const flagResult = await context.api.featureFlags({ projectId }).get({ flagId: String(flagId) }) if (!flagResult.success) { throw new Error(`Failed to get feature flag: ${flagResult.error.message}`) } diff --git a/products/mcp/typescript/src/tools/featureFlags/update.ts b/products/mcp/typescript/src/tools/featureFlags/update.ts index 3769f38a9c..9e887a7193 100644 --- a/products/mcp/typescript/src/tools/featureFlags/update.ts +++ b/products/mcp/typescript/src/tools/featureFlags/update.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { FeatureFlagUpdateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = FeatureFlagUpdateSchema type Params = z.infer -export const updateHandler = async (context: Context, params: Params) => { +export const updateHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { flagKey, data } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/index.ts b/products/mcp/typescript/src/tools/index.ts index 965d587cf5..b589c246a5 100644 --- a/products/mcp/typescript/src/tools/index.ts +++ b/products/mcp/typescript/src/tools/index.ts @@ -1,37 +1,22 @@ -import type { Context, Tool, ToolBase, ZodObjectAny } from './types' - import { ApiClient } from '@/api/client' import { SessionManager } from '@/lib/utils/SessionManager' import { StateManager } from '@/lib/utils/StateManager' +import { hasScopes } from '@/lib/utils/api' import { MemoryCache } from '@/lib/utils/cache/MemoryCache' import { hash } from '@/lib/utils/helper-functions' -import { getToolsForFeatures as getFilteredToolNames, getToolDefinition } from './toolDefinitions' - -import createFeatureFlag from './featureFlags/create' -import deleteFeatureFlag from './featureFlags/delete' -import getAllFeatureFlags from './featureFlags/getAll' -// Feature Flags -import getFeatureFlagDefinition from './featureFlags/getDefinition' -import updateFeatureFlag from './featureFlags/update' - -import getOrganizationDetails from './organizations/getDetails' -// Organizations -import getOrganizations from './organizations/getOrganizations' -import setActiveOrganization from './organizations/setActive' - -import eventDefinitions from './projects/eventDefinitions' -// Projects -import getProjects from './projects/getProjects' -import getProperties from './projects/propertyDefinitions' -import setActiveProject from './projects/setActive' +// Dashboards +import addInsightToDashboard from './dashboards/addInsight' +import createDashboard from './dashboards/create' +import deleteDashboard from './dashboards/delete' +import getDashboard from './dashboards/get' +import getAllDashboards from './dashboards/getAll' +import updateDashboard from './dashboards/update' // Documentation import searchDocs from './documentation/searchDocs' - -import errorDetails from './errorTracking/errorDetails' // Error Tracking +import errorDetails from './errorTracking/errorDetails' import listErrors from './errorTracking/listErrors' - // Experiments import createExperiment from './experiments/create' import deleteExperiment from './experiments/delete' @@ -39,32 +24,33 @@ import getExperiment from './experiments/get' import getAllExperiments from './experiments/getAll' import getExperimentResults from './experiments/getResults' import updateExperiment from './experiments/update' - +// Feature Flags +import createFeatureFlag from './featureFlags/create' +import deleteFeatureFlag from './featureFlags/delete' +import getAllFeatureFlags from './featureFlags/getAll' +import getFeatureFlagDefinition from './featureFlags/getDefinition' +import updateFeatureFlag from './featureFlags/update' +// Insights import createInsight from './insights/create' import deleteInsight from './insights/delete' - import getInsight from './insights/get' -// Insights import getAllInsights from './insights/getAll' import queryInsight from './insights/query' import updateInsight from './insights/update' - -import addInsightToDashboard from './dashboards/addInsight' -import createDashboard from './dashboards/create' -import deleteDashboard from './dashboards/delete' -import getDashboard from './dashboards/get' - -// Dashboards -import getAllDashboards from './dashboards/getAll' -import updateDashboard from './dashboards/update' -import generateHogQLFromQuestion from './query/generateHogQLFromQuestion' -// Query -import queryRun from './query/run' - -import { hasScopes } from '@/lib/utils/api' // LLM Observability import getLLMCosts from './llmAnalytics/getLLMCosts' - +// Organizations +import getOrganizationDetails from './organizations/getDetails' +import getOrganizations from './organizations/getOrganizations' +import setActiveOrganization from './organizations/setActive' +// Projects +import eventDefinitions from './projects/eventDefinitions' +import getProjects from './projects/getProjects' +import getProperties from './projects/propertyDefinitions' +import setActiveProject from './projects/setActive' +// Query +import generateHogQLFromQuestion from './query/generateHogQLFromQuestion' +import queryRun from './query/run' // Surveys import createSurvey from './surveys/create' import deleteSurvey from './surveys/delete' @@ -73,6 +59,9 @@ import getAllSurveys from './surveys/getAll' import surveysGlobalStats from './surveys/global-stats' import surveyStats from './surveys/stats' import updateSurvey from './surveys/update' +// Misc +import { getToolsForFeatures as getFilteredToolNames, getToolDefinition } from './toolDefinitions' +import type { Context, Tool, ToolBase, ZodObjectAny } from './types' // Map of tool names to tool factory functions const TOOL_MAP: Record ToolBase> = { @@ -142,10 +131,7 @@ const TOOL_MAP: Record ToolBase> = { 'survey-stats': surveyStats, } -export const getToolsFromContext = async ( - context: Context, - features?: string[] -): Promise[]> => { +export const getToolsFromContext = async (context: Context, features?: string[]): Promise[]> => { const allowedToolNames = getFilteredToolNames(features) const toolBases: ToolBase[] = [] @@ -173,14 +159,14 @@ export const getToolsFromContext = async ( }) try { - const { scopes } = await context.stateManager.getApiKey() + const { scopes } = (await context.stateManager.getApiKey())! const filteredTools = tools.filter((tool) => { return hasScopes(scopes, tool.scopes) }) return filteredTools - } catch (error) { + } catch { // OAuth tokens don't support /api/personal_api_keys/@current endpoint yet // Return empty tool set until backend OAuth support is added return [] diff --git a/products/mcp/typescript/src/tools/insights/create.ts b/products/mcp/typescript/src/tools/insights/create.ts index 8f2656f438..7ea17c7d4f 100644 --- a/products/mcp/typescript/src/tools/insights/create.ts +++ b/products/mcp/typescript/src/tools/insights/create.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { InsightCreateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = InsightCreateSchema type Params = z.infer -export const createHandler = async (context: Context, params: Params) => { +export const createHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { data } = params const projectId = await context.stateManager.getProjectId() const insightResult = await context.api.insights({ projectId }).create({ data }) diff --git a/products/mcp/typescript/src/tools/insights/delete.ts b/products/mcp/typescript/src/tools/insights/delete.ts index 532b6de123..581e325aa7 100644 --- a/products/mcp/typescript/src/tools/insights/delete.ts +++ b/products/mcp/typescript/src/tools/insights/delete.ts @@ -1,13 +1,15 @@ +import type { z } from 'zod' + import { InsightDeleteSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' + import { resolveInsightId } from './utils' -import type { z } from 'zod' const schema = InsightDeleteSchema type Params = z.infer -export const deleteHandler = async (context: Context, params: Params) => { +export const deleteHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { insightId } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/insights/get.ts b/products/mcp/typescript/src/tools/insights/get.ts index eb6ede50c4..e0e713aab5 100644 --- a/products/mcp/typescript/src/tools/insights/get.ts +++ b/products/mcp/typescript/src/tools/insights/get.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { InsightGetSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = InsightGetSchema type Params = z.infer -export const getHandler = async (context: Context, params: Params) => { +export const getHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { insightId } = params const projectId = await context.stateManager.getProjectId() const insightResult = await context.api.insights({ projectId }).get({ insightId }) diff --git a/products/mcp/typescript/src/tools/insights/getAll.ts b/products/mcp/typescript/src/tools/insights/getAll.ts index 1a65b5fb02..008431654f 100644 --- a/products/mcp/typescript/src/tools/insights/getAll.ts +++ b/products/mcp/typescript/src/tools/insights/getAll.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { InsightGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = InsightGetAllSchema type Params = z.infer -export const getAllHandler = async (context: Context, params: Params) => { +export const getAllHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { data } = params const projectId = await context.stateManager.getProjectId() const insightsResult = await context.api.insights({ projectId }).list({ params: { ...data } }) diff --git a/products/mcp/typescript/src/tools/insights/query.ts b/products/mcp/typescript/src/tools/insights/query.ts index 7b1c126fa9..6cae2d6482 100644 --- a/products/mcp/typescript/src/tools/insights/query.ts +++ b/products/mcp/typescript/src/tools/insights/query.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { InsightQueryInputSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = InsightQueryInputSchema type Params = z.infer -export const queryHandler = async (context: Context, params: Params) => { +export const queryHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { insightId } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/insights/update.ts b/products/mcp/typescript/src/tools/insights/update.ts index 41273c440e..0c847a8d46 100644 --- a/products/mcp/typescript/src/tools/insights/update.ts +++ b/products/mcp/typescript/src/tools/insights/update.ts @@ -1,13 +1,15 @@ +import type { z } from 'zod' + import { InsightUpdateSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' + import { resolveInsightId } from './utils' const schema = InsightUpdateSchema type Params = z.infer -export const updateHandler = async (context: Context, params: Params) => { +export const updateHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { insightId, data } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/insights/utils.ts b/products/mcp/typescript/src/tools/insights/utils.ts index 609a06bbf1..5291565315 100644 --- a/products/mcp/typescript/src/tools/insights/utils.ts +++ b/products/mcp/typescript/src/tools/insights/utils.ts @@ -4,11 +4,7 @@ export const isShortId = (id: string): boolean => { return /^[A-Za-z0-9]{8}$/.test(id) } -export const resolveInsightId = async ( - context: Context, - insightId: string, - projectId: string -): Promise => { +export const resolveInsightId = async (context: Context, insightId: string, projectId: string): Promise => { if (isShortId(insightId)) { const result = await context.api.insights({ projectId }).get({ insightId }) diff --git a/products/mcp/typescript/src/tools/llmAnalytics/getLLMCosts.ts b/products/mcp/typescript/src/tools/llmAnalytics/getLLMCosts.ts index 2a2509a4e3..993733f5b2 100644 --- a/products/mcp/typescript/src/tools/llmAnalytics/getLLMCosts.ts +++ b/products/mcp/typescript/src/tools/llmAnalytics/getLLMCosts.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { LLMAnalyticsGetCostsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = LLMAnalyticsGetCostsSchema type Params = z.infer -export const getLLMCostsHandler = async (context: Context, params: Params) => { +export const getLLMCostsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { projectId, days } = params const trendsQuery = { @@ -31,9 +32,7 @@ export const getLLMCostsHandler = async (context: Context, params: Params) => { }, } - const costsResult = await context.api - .query({ projectId: String(projectId) }) - .execute({ queryBody: trendsQuery }) + const costsResult = await context.api.query({ projectId: String(projectId) }).execute({ queryBody: trendsQuery }) if (!costsResult.success) { throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`) } diff --git a/products/mcp/typescript/src/tools/organizations/getDetails.ts b/products/mcp/typescript/src/tools/organizations/getDetails.ts index 3ef379a783..37ac82202a 100644 --- a/products/mcp/typescript/src/tools/organizations/getDetails.ts +++ b/products/mcp/typescript/src/tools/organizations/getDetails.ts @@ -1,12 +1,9 @@ import { OrganizationGetDetailsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = OrganizationGetDetailsSchema -type Params = z.infer - -export const getDetailsHandler = async (context: Context, _params: Params) => { +export const getDetailsHandler: ToolBase['handler'] = async (context: Context) => { const orgId = await context.stateManager.getOrgID() if (!orgId) { diff --git a/products/mcp/typescript/src/tools/organizations/getOrganizations.ts b/products/mcp/typescript/src/tools/organizations/getOrganizations.ts index 61d640263d..af5fb87196 100644 --- a/products/mcp/typescript/src/tools/organizations/getOrganizations.ts +++ b/products/mcp/typescript/src/tools/organizations/getOrganizations.ts @@ -1,12 +1,9 @@ import { OrganizationGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = OrganizationGetAllSchema -type Params = z.infer - -export const getOrganizationsHandler = async (context: Context, _params: Params) => { +export const getOrganizationsHandler: ToolBase['handler'] = async (context: Context) => { const orgsResult = await context.api.organizations().list() if (!orgsResult.success) { throw new Error(`Failed to get organizations: ${orgsResult.error.message}`) diff --git a/products/mcp/typescript/src/tools/organizations/setActive.ts b/products/mcp/typescript/src/tools/organizations/setActive.ts index ab7d778868..44e1e57241 100644 --- a/products/mcp/typescript/src/tools/organizations/setActive.ts +++ b/products/mcp/typescript/src/tools/organizations/setActive.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { OrganizationSetActiveSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = OrganizationSetActiveSchema type Params = z.infer -export const setActiveHandler = async (context: Context, params: Params) => { +export const setActiveHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { orgId } = params await context.cache.set('orgId', orgId) diff --git a/products/mcp/typescript/src/tools/projects/eventDefinitions.ts b/products/mcp/typescript/src/tools/projects/eventDefinitions.ts index eca3e52299..02613bfb50 100644 --- a/products/mcp/typescript/src/tools/projects/eventDefinitions.ts +++ b/products/mcp/typescript/src/tools/projects/eventDefinitions.ts @@ -1,18 +1,20 @@ +import type { z } from 'zod' + import { EventDefinitionSchema } from '@/schema/properties' import { ProjectEventDefinitionsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ProjectEventDefinitionsSchema type Params = z.infer -export const eventDefinitionsHandler = async (context: Context, _params: Params) => { +export const eventDefinitionsHandler: ToolBase['handler'] = async ( + context: Context, + _params: Params +) => { const projectId = await context.stateManager.getProjectId() - const eventDefsResult = await context.api - .projects() - .eventDefinitions({ projectId, search: _params.q }) + const eventDefsResult = await context.api.projects().eventDefinitions({ projectId, search: _params.q }) if (!eventDefsResult.success) { throw new Error(`Failed to get event definitions: ${eventDefsResult.error.message}`) diff --git a/products/mcp/typescript/src/tools/projects/getProjects.ts b/products/mcp/typescript/src/tools/projects/getProjects.ts index d1d55726a5..b7bcdb5a5b 100644 --- a/products/mcp/typescript/src/tools/projects/getProjects.ts +++ b/products/mcp/typescript/src/tools/projects/getProjects.ts @@ -1,12 +1,9 @@ import { ProjectGetAllSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ProjectGetAllSchema -type Params = z.infer - -export const getProjectsHandler = async (context: Context, _params: Params) => { +export const getProjectsHandler: ToolBase['handler'] = async (context: Context) => { const orgId = await context.stateManager.getOrgID() if (!orgId) { diff --git a/products/mcp/typescript/src/tools/projects/propertyDefinitions.ts b/products/mcp/typescript/src/tools/projects/propertyDefinitions.ts index 725b02b7cd..6f2f49358c 100644 --- a/products/mcp/typescript/src/tools/projects/propertyDefinitions.ts +++ b/products/mcp/typescript/src/tools/projects/propertyDefinitions.ts @@ -1,13 +1,17 @@ +import type { z } from 'zod' + import { PropertyDefinitionSchema } from '@/schema/properties' import { ProjectPropertyDefinitionsInputSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ProjectPropertyDefinitionsInputSchema type Params = z.infer -export const propertyDefinitionsHandler = async (context: Context, params: Params) => { +export const propertyDefinitionsHandler: ToolBase['handler'] = async ( + context: Context, + params: Params +) => { const projectId = await context.stateManager.getProjectId() if (!params.eventName && params.type === 'event') { @@ -25,9 +29,7 @@ export const propertyDefinitionsHandler = async (context: Context, params: Param }) if (!propDefsResult.success) { - throw new Error( - `Failed to get property definitions for ${params.type}s: ${propDefsResult.error.message}` - ) + throw new Error(`Failed to get property definitions for ${params.type}s: ${propDefsResult.error.message}`) } const simplifiedProperties = PropertyDefinitionSchema.array().parse(propDefsResult.data) diff --git a/products/mcp/typescript/src/tools/projects/setActive.ts b/products/mcp/typescript/src/tools/projects/setActive.ts index 356981af7d..a4c8d033aa 100644 --- a/products/mcp/typescript/src/tools/projects/setActive.ts +++ b/products/mcp/typescript/src/tools/projects/setActive.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { ProjectSetActiveSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = ProjectSetActiveSchema type Params = z.infer -export const setActiveHandler = async (context: Context, params: Params) => { +export const setActiveHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { projectId } = params await context.cache.set('projectId', projectId.toString()) diff --git a/products/mcp/typescript/src/tools/query/generateHogQLFromQuestion.ts b/products/mcp/typescript/src/tools/query/generateHogQLFromQuestion.ts index 3ce42bba01..31586f8fac 100644 --- a/products/mcp/typescript/src/tools/query/generateHogQLFromQuestion.ts +++ b/products/mcp/typescript/src/tools/query/generateHogQLFromQuestion.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { InsightGenerateHogQLFromQuestionSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = InsightGenerateHogQLFromQuestionSchema type Params = z.infer -export const generateHogQLHandler = async (context: Context, params: Params) => { +export const generateHogQLHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { question } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/query/run.ts b/products/mcp/typescript/src/tools/query/run.ts index e2201dcbee..7904c4c973 100644 --- a/products/mcp/typescript/src/tools/query/run.ts +++ b/products/mcp/typescript/src/tools/query/run.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { QueryRunInputSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = QueryRunInputSchema type Params = z.infer -export const queryRunHandler = async (context: Context, params: Params) => { +export const queryRunHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { query } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/surveys/create.ts b/products/mcp/typescript/src/tools/surveys/create.ts index ef9b6faefa..567ba7c0b1 100644 --- a/products/mcp/typescript/src/tools/surveys/create.ts +++ b/products/mcp/typescript/src/tools/surveys/create.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { SurveyCreateSchema } from '@/schema/tool-inputs' import { formatSurvey } from '@/tools/surveys/utils/survey-utils' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyCreateSchema type Params = z.infer -export const createHandler = async (context: Context, params: Params) => { +export const createHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() // Process questions to handle branching logic diff --git a/products/mcp/typescript/src/tools/surveys/delete.ts b/products/mcp/typescript/src/tools/surveys/delete.ts index 261e3d9213..68516ba190 100644 --- a/products/mcp/typescript/src/tools/surveys/delete.ts +++ b/products/mcp/typescript/src/tools/surveys/delete.ts @@ -1,11 +1,12 @@ +import type { z } from 'zod' + import { SurveyDeleteSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyDeleteSchema type Params = z.infer -export const deleteHandler = async (context: Context, params: Params) => { +export const deleteHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { surveyId } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/surveys/get.ts b/products/mcp/typescript/src/tools/surveys/get.ts index fe29749056..620451df44 100644 --- a/products/mcp/typescript/src/tools/surveys/get.ts +++ b/products/mcp/typescript/src/tools/surveys/get.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { SurveyGetSchema } from '@/schema/tool-inputs' import { formatSurvey } from '@/tools/surveys/utils/survey-utils' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyGetSchema type Params = z.infer -export const getHandler = async (context: Context, params: Params) => { +export const getHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { surveyId } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/surveys/getAll.ts b/products/mcp/typescript/src/tools/surveys/getAll.ts index e92faaf883..5e50f8d245 100644 --- a/products/mcp/typescript/src/tools/surveys/getAll.ts +++ b/products/mcp/typescript/src/tools/surveys/getAll.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { SurveyGetAllSchema } from '@/schema/tool-inputs' import { formatSurveys } from '@/tools/surveys/utils/survey-utils' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyGetAllSchema type Params = z.infer -export const getAllHandler = async (context: Context, params: Params) => { +export const getAllHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() const surveysResult = await context.api.surveys({ projectId }).list(params ? { params } : {}) diff --git a/products/mcp/typescript/src/tools/surveys/global-stats.ts b/products/mcp/typescript/src/tools/surveys/global-stats.ts index 30bccd8d64..f7336222cd 100644 --- a/products/mcp/typescript/src/tools/surveys/global-stats.ts +++ b/products/mcp/typescript/src/tools/surveys/global-stats.ts @@ -1,11 +1,12 @@ +import type { z } from 'zod' + import { SurveyGlobalStatsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyGlobalStatsSchema type Params = z.infer -export const globalStatsHandler = async (context: Context, params: Params) => { +export const globalStatsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() const result = await context.api.surveys({ projectId }).globalStats({ params }) diff --git a/products/mcp/typescript/src/tools/surveys/stats.ts b/products/mcp/typescript/src/tools/surveys/stats.ts index 14b4767813..eafd1b2e56 100644 --- a/products/mcp/typescript/src/tools/surveys/stats.ts +++ b/products/mcp/typescript/src/tools/surveys/stats.ts @@ -1,11 +1,12 @@ +import type { z } from 'zod' + import { SurveyStatsSchema } from '@/schema/tool-inputs' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyStatsSchema type Params = z.infer -export const statsHandler = async (context: Context, params: Params) => { +export const statsHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const projectId = await context.stateManager.getProjectId() const result = await context.api.surveys({ projectId }).stats({ diff --git a/products/mcp/typescript/src/tools/surveys/update.ts b/products/mcp/typescript/src/tools/surveys/update.ts index 656e6e88d8..0b1225a61d 100644 --- a/products/mcp/typescript/src/tools/surveys/update.ts +++ b/products/mcp/typescript/src/tools/surveys/update.ts @@ -1,12 +1,13 @@ +import type { z } from 'zod' + import { SurveyUpdateSchema } from '@/schema/tool-inputs' import { formatSurvey } from '@/tools/surveys/utils/survey-utils' import type { Context, ToolBase } from '@/tools/types' -import type { z } from 'zod' const schema = SurveyUpdateSchema type Params = z.infer -export const updateHandler = async (context: Context, params: Params) => { +export const updateHandler: ToolBase['handler'] = async (context: Context, params: Params) => { const { surveyId, ...data } = params const projectId = await context.stateManager.getProjectId() diff --git a/products/mcp/typescript/src/tools/surveys/utils/survey-utils.ts b/products/mcp/typescript/src/tools/surveys/utils/survey-utils.ts index 528f30301e..a86897b038 100644 --- a/products/mcp/typescript/src/tools/surveys/utils/survey-utils.ts +++ b/products/mcp/typescript/src/tools/surveys/utils/survey-utils.ts @@ -12,11 +12,7 @@ export interface FormattedSurvey extends Omit { /** * Formats a survey with consistent status logic and additional fields */ -export function formatSurvey( - survey: SurveyData, - context: Context, - projectId: string -): FormattedSurvey { +export function formatSurvey(survey: SurveyData, context: Context, projectId: string): FormattedSurvey { const status = survey.archived ? 'archived' : survey.start_date === null || survey.start_date === undefined @@ -43,10 +39,6 @@ export function formatSurvey( /** * Formats multiple surveys consistently */ -export function formatSurveys( - surveys: SurveyData[], - context: Context, - projectId: string -): FormattedSurvey[] { +export function formatSurveys(surveys: SurveyData[], context: Context, projectId: string): FormattedSurvey[] { return surveys.map((survey) => formatSurvey(survey, context, projectId)) } diff --git a/products/mcp/typescript/src/tools/toolDefinitions.ts b/products/mcp/typescript/src/tools/toolDefinitions.ts index 6905cb18d5..d69c604e04 100644 --- a/products/mcp/typescript/src/tools/toolDefinitions.ts +++ b/products/mcp/typescript/src/tools/toolDefinitions.ts @@ -1,4 +1,5 @@ import z from 'zod' + import toolDefinitionsJson from '../../../schema/tool-definitions.json' export const ToolDefinitionSchema = z.object({ diff --git a/products/mcp/typescript/src/tools/types.ts b/products/mcp/typescript/src/tools/types.ts index 2078476901..84623368f3 100644 --- a/products/mcp/typescript/src/tools/types.ts +++ b/products/mcp/typescript/src/tools/types.ts @@ -1,10 +1,11 @@ +import type { z } from 'zod' + import type { ApiClient } from '@/api/client' import type { PrefixedString } from '@/lib/types' -import type { StateManager } from '@/lib/utils/StateManager' import type { SessionManager } from '@/lib/utils/SessionManager' +import type { StateManager } from '@/lib/utils/StateManager' import type { ScopedCache } from '@/lib/utils/cache/ScopedCache' import type { ApiRedactedPersonalApiKey } from '@/schema/api' -import type { z } from 'zod' export type CloudRegion = 'us' | 'eu' diff --git a/products/mcp/typescript/tests/README.md b/products/mcp/typescript/tests/README.md index 4a8149baee..fa78f7ef80 100644 --- a/products/mcp/typescript/tests/README.md +++ b/products/mcp/typescript/tests/README.md @@ -6,11 +6,11 @@ These tests should verify that the API works as intended. 1. **Configure environment:** - ```bash - cp .env.test.example .env.test - ``` + ```bash + cp .env.test.example .env.test + ``` - Edit `.env.test` and set an api token and base url for your local PostHog instance + Edit `.env.test` and set an api token and base url for your local PostHog instance ## Running Tests diff --git a/products/mcp/typescript/tests/api/client.integration.test.ts b/products/mcp/typescript/tests/api/client.integration.test.ts index fd03d212e7..4266ce3320 100644 --- a/products/mcp/typescript/tests/api/client.integration.test.ts +++ b/products/mcp/typescript/tests/api/client.integration.test.ts @@ -1,6 +1,7 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { ApiClient } from '@/api/client' import type { CreateInsightInput } from '@/schema/insights' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' const API_BASE_URL = process.env.TEST_POSTHOG_API_BASE_URL || 'http://localhost:8010' const API_TOKEN = process.env.TEST_POSTHOG_PERSONAL_API_KEY @@ -163,31 +164,28 @@ describe('API Client Integration Tests', { concurrent: false }, () => { } }) - it.each(['event', 'person'] as const)( - 'should get property definitions for %s type', - async (type) => { - const result = await client.projects().propertyDefinitions({ - projectId: testProjectId, - type, - eventNames: type === 'event' ? ['$pageview'] : undefined, - excludeCoreProperties: false, - filterByEventNames: type === 'event', - isFeatureFlag: false, - limit: 100, - }) + it.each(['event', 'person'] as const)('should get property definitions for %s type', async (type) => { + const result = await client.projects().propertyDefinitions({ + projectId: testProjectId, + type, + eventNames: type === 'event' ? ['$pageview'] : undefined, + excludeCoreProperties: false, + filterByEventNames: type === 'event', + isFeatureFlag: false, + limit: 100, + }) - expect(result.success).toBe(true) + expect(result.success).toBe(true) - if (result.success) { - expect(Array.isArray(result.data)).toBe(true) - if (result.data.length > 0) { - const propDef = result.data[0] - expect(propDef).toHaveProperty('id') - expect(propDef).toHaveProperty('name') - } + if (result.success) { + expect(Array.isArray(result.data)).toBe(true) + if (result.data.length > 0) { + const propDef = result.data[0] + expect(propDef).toHaveProperty('id') + expect(propDef).toHaveProperty('name') } } - ) + }) it('should get event definitions', async () => { const result = await client.projects().eventDefinitions({ projectId: testProjectId }) @@ -279,9 +277,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { createdResources.featureFlags.push(flagId) // Get by ID - const getResult = await client - .featureFlags({ projectId: testProjectId }) - .get({ flagId }) + const getResult = await client.featureFlags({ projectId: testProjectId }).get({ flagId }) expect(getResult.success).toBe(true) if (getResult.success) { @@ -290,9 +286,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { } // Find by key - const findResult = await client - .featureFlags({ projectId: testProjectId }) - .findByKey({ key: testKey }) + const findResult = await client.featureFlags({ projectId: testProjectId }).findByKey({ key: testKey }) expect(findResult.success).toBe(true) if (findResult.success && findResult.data) { @@ -301,21 +295,17 @@ describe('API Client Integration Tests', { concurrent: false }, () => { } // Update - const updateResult = await client - .featureFlags({ projectId: testProjectId }) - .update({ - key: testKey, - data: { - name: 'Updated Test Flag', - active: false, - }, - }) + const updateResult = await client.featureFlags({ projectId: testProjectId }).update({ + key: testKey, + data: { + name: 'Updated Test Flag', + active: false, + }, + }) expect(updateResult.success).toBe(true) // Verify update - const updatedGetResult = await client - .featureFlags({ projectId: testProjectId }) - .get({ flagId }) + const updatedGetResult = await client.featureFlags({ projectId: testProjectId }).get({ flagId }) if (updatedGetResult.success) { expect(updatedGetResult.data.name).toBe('Updated Test Flag') expect(updatedGetResult.data.active).toBe(false) @@ -344,9 +334,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { }) it.skip('should execute SQL insight query', async () => { - const result = await client - .insights({ projectId: testProjectId }) - .sqlInsight({ query: 'SELECT 1 as test' }) + const result = await client.insights({ projectId: testProjectId }).sqlInsight({ query: 'SELECT 1 as test' }) if (!result.success) { console.error('Failed to execute SQL insight:', (result as any).error) @@ -523,8 +511,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { ], breakdownFilter: { breakdown_type: breakdownType, - breakdown: - breakdownType === 'event' ? '$current_url' : '$browser', + breakdown: breakdownType === 'event' ? '$current_url' : '$browser', breakdown_limit: 10, }, properties: [], @@ -828,8 +815,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { }, ], funnelsFilter: { - funnelWindowInterval: - unit === 'minute' ? 30 : unit === 'hour' ? 2 : 7, + funnelWindowInterval: unit === 'minute' ? 30 : unit === 'hour' ? 2 : 7, funnelWindowIntervalUnit: unit, }, properties: [], @@ -1005,9 +991,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { createdResources.dashboards.push(dashboardId) // Get - const getResult = await client - .dashboards({ projectId: testProjectId }) - .get({ dashboardId }) + const getResult = await client.dashboards({ projectId: testProjectId }).get({ dashboardId }) expect(getResult.success).toBe(true) if (getResult.success) { @@ -1043,9 +1027,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { status: 'active', } - const result = await client - .query({ projectId: testProjectId }) - .execute({ queryBody: errorQuery }) + const result = await client.query({ projectId: testProjectId }).execute({ queryBody: errorQuery }) if (!result.success) { console.error('Failed to execute error query:', (result as any).error) @@ -1082,9 +1064,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { }, } - const result = await client - .query({ projectId: testProjectId }) - .execute({ queryBody: trendsQuery }) + const result = await client.query({ projectId: testProjectId }).execute({ queryBody: trendsQuery }) expect(result.success).toBe(true) @@ -1126,7 +1106,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { description?: string }> } = {} - ) => { + ): Promise => { const timestamp = Date.now() const createResult = await client.experiments({ projectId: testProjectId }).create({ name: options.name || `Test Experiment ${timestamp}`, @@ -1160,9 +1140,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { return createResult.data } - throw new Error( - `Failed to create test experiment: ${(createResult as any).error?.message}` - ) + throw new Error(`Failed to create test experiment: ${(createResult as any).error?.message}`) } it.skip('should list experiments', async () => { @@ -1344,19 +1322,17 @@ describe('API Client Integration Tests', { concurrent: false }, () => { }) // Try to get exposures (may not have data immediately) - const exposureResult = await client - .experiments({ projectId: testProjectId }) - .getExposures({ - experimentId: experiment.id, - refresh: true, - }) + const exposureResult = await client.experiments({ projectId: testProjectId }).getExposures({ + experimentId: experiment.id, + refresh: true, + }) // Should succeed even if no exposure data yet expect(exposureResult.success).toBe(true) if (exposureResult.success) { expect(exposureResult.data).toHaveProperty('exposures') - expect(exposureResult.data.exposures).toBeDefined() + expect(exposureResult.data.exposures).toBeTruthy() } }) @@ -1366,12 +1342,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => { draft: true, }) - const exposureResult = await client - .experiments({ projectId: testProjectId }) - .getExposures({ - experimentId: experiment.id, - refresh: false, - }) + const exposureResult = await client.experiments({ projectId: testProjectId }).getExposures({ + experimentId: experiment.id, + refresh: false, + }) expect(exposureResult.success).toBe(false) expect((exposureResult as any).error.message).toContain('has not started yet') @@ -1393,12 +1367,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => { }) // Try to get metric results - const metricsResult = await client - .experiments({ projectId: testProjectId }) - .getMetricResults({ - experimentId: experiment.id, - refresh: true, - }) + const metricsResult = await client.experiments({ projectId: testProjectId }).getMetricResults({ + experimentId: experiment.id, + refresh: true, + }) expect(metricsResult.success).toBe(true) @@ -1417,12 +1389,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => { draft: true, }) - const metricsResult = await client - .experiments({ projectId: testProjectId }) - .getMetricResults({ - experimentId: experiment.id, - refresh: false, - }) + const metricsResult = await client.experiments({ projectId: testProjectId }).getMetricResults({ + experimentId: experiment.id, + refresh: false, + }) expect(metricsResult.success).toBe(false) expect((metricsResult as any).error.message).toContain('has not started yet') @@ -1466,12 +1436,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { if (updateResult.success) { expect(updateResult.data.parameters?.feature_flag_variants).toHaveLength(4) const variants = updateResult.data.parameters?.feature_flag_variants || [] - expect(variants.map((v) => v.key)).toEqual([ - 'control', - 'variant_a', - 'variant_b', - 'variant_c', - ]) + expect(variants.map((v) => v.key)).toEqual(['control', 'variant_a', 'variant_b', 'variant_c']) } }) @@ -1563,9 +1528,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { createdResources.experiments.push(experimentId) // READ - const getResult = await client - .experiments({ projectId: testProjectId }) - .get({ experimentId }) + const getResult = await client.experiments({ projectId: testProjectId }).get({ experimentId }) expect(getResult.success).toBe(true) @@ -1592,9 +1555,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { } // DELETE - const deleteResult = await client - .experiments({ projectId: testProjectId }) - .delete({ experimentId }) + const deleteResult = await client.experiments({ projectId: testProjectId }).delete({ experimentId }) expect(deleteResult.success).toBe(true) if (deleteResult.success) { @@ -1603,9 +1564,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => { } // Verify deletion worked - const getAfterDeleteResult = await client - .experiments({ projectId: testProjectId }) - .get({ experimentId }) + const getAfterDeleteResult = await client.experiments({ projectId: testProjectId }).get({ experimentId }) expect(getAfterDeleteResult.success).toBe(false) diff --git a/products/mcp/typescript/tests/integration/example-resources.test.ts b/products/mcp/typescript/tests/integration/example-resources.test.ts index 48f29e72e8..0720950225 100644 --- a/products/mcp/typescript/tests/integration/example-resources.test.ts +++ b/products/mcp/typescript/tests/integration/example-resources.test.ts @@ -1,14 +1,15 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { describe, expect, it, beforeEach } from 'vitest' +import { strFromU8, unzipSync } from 'fflate' +import { beforeEach, describe, expect, it } from 'vitest' + import { registerIntegrationResources } from '@/resources/integration' -import { ResourceUri } from '@/resources/integration/index' import { - getSupportedFrameworks, EXAMPLES_MARKDOWN_URL, FRAMEWORK_MARKDOWN_FILES, + getSupportedFrameworks, } from '@/resources/integration/framework-mappings' +import { ResourceUri } from '@/resources/integration/index' import type { Context } from '@/tools/types' -import { unzipSync, strFromU8 } from 'fflate' const createMockContext = (): Context => ({ api: {} as any, @@ -41,7 +42,7 @@ describe('Example Resources - Markdown Artifact Loading', () => { const uint8Array = new Uint8Array(arrayBuffer) const unzipped = unzipSync(uint8Array) - expect(unzipped).toBeDefined() + expect(unzipped).toBeTruthy() expect(Object.keys(unzipped).length).toBeGreaterThan(0) }, 30000) // 30 second timeout for network request @@ -54,10 +55,9 @@ describe('Example Resources - Markdown Artifact Loading', () => { const frameworks = getSupportedFrameworks() for (const framework of frameworks) { - const filename = - FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES] + const filename = FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES] const fileData = unzipped[filename] - expect(fileData).toBeDefined() + expect(fileData).toBeTruthy() expect(fileData!.length).toBeGreaterThan(0) } }, 30000) @@ -71,10 +71,9 @@ describe('Example Resources - Markdown Artifact Loading', () => { const frameworks = getSupportedFrameworks() for (const framework of frameworks) { - const filename = - FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES] + const filename = FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES] const markdownData = unzipped[filename] - expect(markdownData).toBeDefined() + expect(markdownData).toBeTruthy() const markdown = strFromU8(markdownData!) expect(markdown).toBeTruthy() @@ -95,17 +94,15 @@ describe('Example Resources - Markdown Artifact Loading', () => { .startsWith(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', '')) ) as any - expect(exampleTemplate).toBeDefined() + expect(exampleTemplate).toBeTruthy() const frameworks = getSupportedFrameworks() for (const framework of frameworks) { - const uri = new URL( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework) - ) + const uri = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)) const result = await exampleTemplate.readCallback(uri, { framework }) - expect(result).toBeDefined() + expect(result).toBeTruthy() expect(result.contents).toHaveLength(1) expect(result.contents[0].text).toBeTruthy() expect(result.contents[0].text).toContain('# PostHog') @@ -122,13 +119,11 @@ describe('Example Resources - Markdown Artifact Loading', () => { ) as any const invalidFramework = 'invalid-framework-xyz' - const uri = new URL( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', invalidFramework) - ) + const uri = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', invalidFramework)) - await expect( - exampleTemplate.readCallback(uri, { framework: invalidFramework }) - ).rejects.toThrow(/is not supported yet/) + await expect(exampleTemplate.readCallback(uri, { framework: invalidFramework })).rejects.toThrow( + /is not supported yet/ + ) }, 30000) it('should cache the markdown ZIP and reuse it', async () => { @@ -142,16 +137,12 @@ describe('Example Resources - Markdown Artifact Loading', () => { const framework = 'nextjs-app-router' // First call should fetch and cache - const uri1 = new URL( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework) - ) + const uri1 = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)) const result1 = await exampleTemplate.readCallback(uri1, { framework }) expect(result1.contents[0].text).toBeTruthy() // Second call should use cache (we can't directly verify caching, but we verify it works) - const uri2 = new URL( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework) - ) + const uri2 = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)) const result2 = await exampleTemplate.readCallback(uri2, { framework }) expect(result2.contents[0].text).toBeTruthy() diff --git a/products/mcp/typescript/tests/integration/feature-routing.test.ts b/products/mcp/typescript/tests/integration/feature-routing.test.ts index 83b7cab727..24ea84e67b 100644 --- a/products/mcp/typescript/tests/integration/feature-routing.test.ts +++ b/products/mcp/typescript/tests/integration/feature-routing.test.ts @@ -1,7 +1,8 @@ +import { describe, expect, it } from 'vitest' + import { SessionManager } from '@/lib/utils/SessionManager' import { getToolsFromContext } from '@/tools' import type { Context } from '@/tools/types' -import { describe, expect, it } from 'vitest' const createMockContext = (): Context => ({ api: {} as any, diff --git a/products/mcp/typescript/tests/shared/test-utils.ts b/products/mcp/typescript/tests/shared/test-utils.ts index fec1c59295..99b70f6232 100644 --- a/products/mcp/typescript/tests/shared/test-utils.ts +++ b/products/mcp/typescript/tests/shared/test-utils.ts @@ -1,10 +1,11 @@ +import { expect } from 'vitest' + import { ApiClient } from '@/api/client' import { SessionManager } from '@/lib/utils/SessionManager' import { StateManager } from '@/lib/utils/StateManager' import { MemoryCache } from '@/lib/utils/cache/MemoryCache' import type { InsightQuery } from '@/schema/query' import type { Context } from '@/tools/types' -import { expect } from 'vitest' export const API_BASE_URL = process.env.TEST_POSTHOG_API_BASE_URL || 'http://localhost:8010' export const API_TOKEN = process.env.TEST_POSTHOG_PERSONAL_API_KEY @@ -18,7 +19,7 @@ export interface CreatedResources { surveys: string[] } -export function validateEnvironmentVariables() { +export function validateEnvironmentVariables(): void { if (!API_TOKEN) { throw new Error('TEST_POSTHOG_PERSONAL_API_KEY environment variable is required') } @@ -54,7 +55,7 @@ export function createTestContext(client: ApiClient): Context { return context } -export async function setActiveProjectAndOrg(context: Context, projectId: string, orgId: string) { +export async function setActiveProjectAndOrg(context: Context, projectId: string, orgId: string): Promise { const cache = context.cache await cache.set('projectId', projectId) await cache.set('orgId', orgId) @@ -64,7 +65,7 @@ export async function cleanupResources( client: ApiClient, projectId: string, resources: CreatedResources -) { +): Promise { for (const flagId of resources.featureFlags) { try { await client.featureFlags({ projectId }).delete({ flagId }) @@ -102,8 +103,8 @@ export async function cleanupResources( resources.surveys = [] } -export function parseToolResponse(result: any) { - expect(result.content).toBeDefined() +export function parseToolResponse(result: any): any { + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') return JSON.parse(result.content[0].text) } @@ -143,12 +144,7 @@ export const SAMPLE_HOGQL_QUERIES: Record = { }, } -type SampleTrendQuery = - | 'basicPageviews' - | 'uniqueUsers' - | 'multipleEvents' - | 'withBreakdown' - | 'withPropertyFilter' +type SampleTrendQuery = 'basicPageviews' | 'uniqueUsers' | 'multipleEvents' | 'withBreakdown' | 'withPropertyFilter' export const SAMPLE_TREND_QUERIES: Record = { basicPageviews: { diff --git a/products/mcp/typescript/tests/tools/dashboards.integration.test.ts b/products/mcp/typescript/tests/tools/dashboards.integration.test.ts index f346687aa9..a07cfcf0b9 100644 --- a/products/mcp/typescript/tests/tools/dashboards.integration.test.ts +++ b/products/mcp/typescript/tests/tools/dashboards.integration.test.ts @@ -1,21 +1,22 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, - setActiveProjectAndOrg, - cleanupResources, - parseToolResponse, generateUniqueKey, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, + parseToolResponse, + setActiveProjectAndOrg, + validateEnvironmentVariables, } from '@/shared/test-utils' import createDashboardTool from '@/tools/dashboards/create' -import updateDashboardTool from '@/tools/dashboards/update' import deleteDashboardTool from '@/tools/dashboards/delete' -import getAllDashboardsTool from '@/tools/dashboards/getAll' import getDashboardTool from '@/tools/dashboards/get' +import getAllDashboardsTool from '@/tools/dashboards/getAll' +import updateDashboardTool from '@/tools/dashboards/update' import type { Context } from '@/tools/types' describe('Dashboards', { concurrent: false }, () => { @@ -53,7 +54,7 @@ describe('Dashboards', { concurrent: false }, () => { const result = await createTool.handler(context, params) const dashboardData = parseToolResponse(result) - expect(dashboardData.id).toBeDefined() + expect(dashboardData.id).toBeTruthy() expect(dashboardData.name).toBe(params.data.name) expect(dashboardData.url).toContain('/dashboard/') @@ -73,7 +74,7 @@ describe('Dashboards', { concurrent: false }, () => { const result = await createTool.handler(context, params) const dashboardData = parseToolResponse(result) - expect(dashboardData.id).toBeDefined() + expect(dashboardData.id).toBeTruthy() expect(dashboardData.name).toBe(params.data.name) createdResources.dashboards.push(dashboardData.id) diff --git a/products/mcp/typescript/tests/tools/documentation.integration.test.ts b/products/mcp/typescript/tests/tools/documentation.integration.test.ts index 52bd4ee200..529a2190f2 100644 --- a/products/mcp/typescript/tests/tools/documentation.integration.test.ts +++ b/products/mcp/typescript/tests/tools/documentation.integration.test.ts @@ -1,13 +1,14 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, setActiveProjectAndOrg, - cleanupResources, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, + validateEnvironmentVariables, } from '@/shared/test-utils' import searchDocsTool from '@/tools/documentation/searchDocs' import type { Context } from '@/tools/types' @@ -54,7 +55,7 @@ describe('Documentation', { concurrent: false }, () => { }) expect(result.content[0].type).toBe('text') - expect(result.content[0].text).toBeDefined() + expect(result.content[0].text).toBeTruthy() expect(result.content[0].text.length).toBeGreaterThan(0) }) @@ -64,7 +65,7 @@ describe('Documentation', { concurrent: false }, () => { }) expect(result.content[0].type).toBe('text') - expect(result.content[0].text).toBeDefined() + expect(result.content[0].text).toBeTruthy() expect(result.content[0].text.length).toBeGreaterThan(0) }) @@ -74,7 +75,7 @@ describe('Documentation', { concurrent: false }, () => { }) expect(result.content[0].type).toBe('text') - expect(result.content[0].text).toBeDefined() + expect(result.content[0].text).toBeTruthy() }) }) @@ -86,7 +87,7 @@ describe('Documentation', { concurrent: false }, () => { await searchTool.handler(context, { query: '' }) expect.fail('Should have thrown validation error') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) diff --git a/products/mcp/typescript/tests/tools/errorTracking.integration.test.ts b/products/mcp/typescript/tests/tools/errorTracking.integration.test.ts index a2050b9152..991a2d51f3 100644 --- a/products/mcp/typescript/tests/tools/errorTracking.integration.test.ts +++ b/products/mcp/typescript/tests/tools/errorTracking.integration.test.ts @@ -1,19 +1,20 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + +import { OrderByErrors, OrderDirectionErrors, StatusErrors } from '@/schema/errors' import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, - setActiveProjectAndOrg, - cleanupResources, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, parseToolResponse, + setActiveProjectAndOrg, + validateEnvironmentVariables, } from '@/shared/test-utils' -import listErrorsTool from '@/tools/errorTracking/listErrors' import errorDetailsTool from '@/tools/errorTracking/errorDetails' +import listErrorsTool from '@/tools/errorTracking/listErrors' import type { Context } from '@/tools/types' -import { OrderByErrors, OrderDirectionErrors, StatusErrors } from '@/schema/errors' describe('Error Tracking', { concurrent: false }, () => { let context: Context diff --git a/products/mcp/typescript/tests/tools/experiments.integration.test.ts b/products/mcp/typescript/tests/tools/experiments.integration.test.ts index b309efef0a..744f1e8859 100644 --- a/products/mcp/typescript/tests/tools/experiments.integration.test.ts +++ b/products/mcp/typescript/tests/tools/experiments.integration.test.ts @@ -1,20 +1,21 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, - setActiveProjectAndOrg, - cleanupResources, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, - parseToolResponse, generateUniqueKey, + parseToolResponse, + setActiveProjectAndOrg, + validateEnvironmentVariables, } from '@/shared/test-utils' import createExperimentTool from '@/tools/experiments/create' import deleteExperimentTool from '@/tools/experiments/delete' -import getAllExperimentsTool from '@/tools/experiments/getAll' import getExperimentTool from '@/tools/experiments/get' +import getAllExperimentsTool from '@/tools/experiments/getAll' import getExperimentResultsTool from '@/tools/experiments/getResults' import updateExperimentTool from '@/tools/experiments/update' import type { Context } from '@/tools/types' @@ -30,7 +31,7 @@ describe('Experiments', { concurrent: false }, () => { const createdExperiments: number[] = [] // Helper function to track created experiments and their feature flags - const trackExperiment = (experiment: any) => { + const trackExperiment = (experiment: any): void => { if (experiment.id) { createdExperiments.push(experiment.id) } @@ -80,7 +81,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.name).toBe(params.name) expect(experiment.feature_flag_key).toBe(params.feature_flag_key) expect(experiment.start_date).toBeNull() // Draft experiments have no start date @@ -103,7 +104,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.name).toBe(params.name) expect(experiment.feature_flag_key).toBe(params.feature_flag_key) @@ -127,7 +128,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.parameters?.feature_flag_variants).toHaveLength(3) expect(experiment.parameters?.feature_flag_variants?.[0]?.key).toBe('control') expect(experiment.parameters?.feature_flag_variants?.[0]?.rollout_percentage).toBe(33) @@ -156,7 +157,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(1) trackExperiment(experiment) @@ -183,7 +184,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(1) trackExperiment(experiment) @@ -209,7 +210,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(1) trackExperiment(experiment) @@ -252,7 +253,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(2) expect(experiment.metrics_secondary).toHaveLength(2) @@ -272,7 +273,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) @@ -290,7 +291,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) @@ -306,7 +307,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) }) @@ -343,7 +344,7 @@ describe('Experiments', { concurrent: false }, () => { // Verify our test experiments are in the list for (const testExp of testExperiments) { const found = allExperiments.find((e: any) => e.id === testExp.id) - expect(found).toBeDefined() + expect(found).toBeTruthy() } }) @@ -391,9 +392,7 @@ describe('Experiments', { concurrent: false }, () => { it('should handle non-existent experiment ID', async () => { const nonExistentId = 999999 - await expect( - getTool.handler(context, { experimentId: nonExistentId }) - ).rejects.toThrow() + await expect(getTool.handler(context, { experimentId: nonExistentId })).rejects.toThrow() }) }) @@ -521,7 +520,7 @@ describe('Experiments', { concurrent: false }, () => { trackExperiment(createdExperiment) // Verify creation - expect(createdExperiment.id).toBeDefined() + expect(createdExperiment.id).toBeTruthy() expect(createdExperiment.name).toBe(createParams.name) expect(createdExperiment.parameters?.feature_flag_variants).toHaveLength(2) expect(createdExperiment.metrics).toHaveLength(2) @@ -538,7 +537,7 @@ describe('Experiments', { concurrent: false }, () => { const listResult = await getAllTool.handler(context, {}) const allExperiments = parseToolResponse(listResult) const found = allExperiments.find((e: any) => e.id === createdExperiment.id) - expect(found).toBeDefined() + expect(found).toBeTruthy() }) it('should create experiment with complex funnel metrics', async () => { @@ -579,7 +578,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(1) expect(experiment.metrics_secondary).toHaveLength(1) @@ -603,7 +602,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) @@ -621,7 +620,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) @@ -644,7 +643,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics || []).toHaveLength(0) expect(experiment.metrics_secondary || []).toHaveLength(0) @@ -687,7 +686,7 @@ describe('Experiments', { concurrent: false }, () => { trackExperiment(experiment) } catch (error) { // Expected for invalid configuration - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) @@ -710,7 +709,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() expect(experiment.metrics).toHaveLength(1) trackExperiment(experiment) @@ -736,7 +735,7 @@ describe('Experiments', { concurrent: false }, () => { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) }) @@ -754,11 +753,11 @@ describe('Experiments', { concurrent: false }, () => { try { const result = await createTool.handler(context, params as any) const experiment = parseToolResponse(result) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) } catch (error) { // Some APIs might reject very long names - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) @@ -779,7 +778,7 @@ describe('Experiments', { concurrent: false }, () => { const createResult = await createTool.handler(context, createParams as any) const experiment = parseToolResponse(createResult) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) // Delete the experiment @@ -811,7 +810,7 @@ describe('Experiments', { concurrent: false }, () => { await deleteTool.handler(context, deleteParams) expect.fail('Should have thrown an error for invalid experiment ID') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() expect((error as Error).message).toContain('Failed to delete experiment') } }) @@ -828,7 +827,7 @@ describe('Experiments', { concurrent: false }, () => { const createResult = await createTool.handler(context, createParams as any) const experiment = parseToolResponse(createResult) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) // Delete the experiment twice @@ -844,7 +843,7 @@ describe('Experiments', { concurrent: false }, () => { await deleteTool.handler(context, deleteParams) expect.fail('Should have thrown an error for already deleted experiment') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() expect((error as Error).message).toContain('Failed to delete experiment') expect((error as Error).message).toContain('404') } @@ -866,7 +865,7 @@ describe('Experiments', { concurrent: false }, () => { await deleteTool.handler(context, {} as any) expect.fail('Should have thrown validation error for missing experimentId') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) @@ -888,7 +887,7 @@ describe('Experiments', { concurrent: false }, () => { const createResult = await createTool.handler(context, createParams as any) const experiment = parseToolResponse(createResult) - expect(experiment.id).toBeDefined() + expect(experiment.id).toBeTruthy() trackExperiment(experiment) // Update basic fields @@ -937,7 +936,7 @@ describe('Experiments', { concurrent: false }, () => { const updateResult = await updateTool.handler(context, launchParams) const launchedExperiment = parseToolResponse(updateResult) - expect(launchedExperiment.start_date).toBeDefined() // Running experiments have start date + expect(launchedExperiment.start_date).toBeTruthy() // Running experiments have start date expect(launchedExperiment.end_date).toBeNull() // But no end date yet trackExperiment(experiment) @@ -970,7 +969,7 @@ describe('Experiments', { concurrent: false }, () => { const updateResult = await updateTool.handler(context, stopParams) const stoppedExperiment = parseToolResponse(updateResult) - expect(stoppedExperiment.end_date).toBeDefined() + expect(stoppedExperiment.end_date).toBeTruthy() // Note: API may not set conclusion field automatically, it depends on the backend implementation // The important thing is that end_date is set, indicating the experiment is stopped @@ -1018,7 +1017,7 @@ describe('Experiments', { concurrent: false }, () => { expect(restartedExperiment.end_date).toBeNull() expect(restartedExperiment.conclusion).toBeNull() expect(restartedExperiment.conclusion_comment).toBeNull() - expect(restartedExperiment.start_date).toBeDefined() // Restarted experiments have start date + expect(restartedExperiment.start_date).toBeTruthy() // Restarted experiments have start date expect(restartedExperiment.end_date).toBeNull() // But no end date trackExperiment(experiment) @@ -1157,7 +1156,7 @@ describe('Experiments', { concurrent: false }, () => { await updateTool.handler(context, updateParams) expect.fail('Should have thrown an error for invalid experiment ID') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() expect((error as Error).message).toContain('Failed to update experiment') } }) @@ -1167,7 +1166,7 @@ describe('Experiments', { concurrent: false }, () => { await updateTool.handler(context, { data: { name: 'Test' } } as any) expect.fail('Should have thrown validation error for missing experimentId') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) @@ -1239,12 +1238,12 @@ describe('Experiments', { concurrent: false }, () => { const experiment = parseToolResponse(result) // Check actual date fields instead of computed status - expect(experiment.start_date).toBeDefined() // Should have start date if launched + expect(experiment.start_date).toBeTruthy() // Should have start date if launched trackExperiment(experiment) } catch (error) { // Some environments might not allow immediate launch - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) diff --git a/products/mcp/typescript/tests/tools/featureFlags.integration.test.ts b/products/mcp/typescript/tests/tools/featureFlags.integration.test.ts index a87afb0764..b6fe27ed90 100644 --- a/products/mcp/typescript/tests/tools/featureFlags.integration.test.ts +++ b/products/mcp/typescript/tests/tools/featureFlags.integration.test.ts @@ -1,21 +1,22 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, - setActiveProjectAndOrg, - cleanupResources, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, - parseToolResponse, generateUniqueKey, + parseToolResponse, + setActiveProjectAndOrg, + validateEnvironmentVariables, } from '@/shared/test-utils' import createFeatureFlagTool from '@/tools/featureFlags/create' -import updateFeatureFlagTool from '@/tools/featureFlags/update' import deleteFeatureFlagTool from '@/tools/featureFlags/delete' import getAllFeatureFlagsTool from '@/tools/featureFlags/getAll' import getFeatureFlagDefinitionTool from '@/tools/featureFlags/getDefinition' +import updateFeatureFlagTool from '@/tools/featureFlags/update' import type { Context } from '@/tools/types' describe('Feature Flags', { concurrent: false }, () => { @@ -53,7 +54,7 @@ describe('Feature Flags', { concurrent: false }, () => { const result = await createTool.handler(context, params) const flagData = parseToolResponse(result) - expect(flagData.id).toBeDefined() + expect(flagData.id).toBeTruthy() expect(flagData.key).toBe(params.key) expect(flagData.name).toBe(params.name) expect(flagData.active).toBe(params.active) @@ -75,7 +76,7 @@ describe('Feature Flags', { concurrent: false }, () => { const result = await createTool.handler(context, params) const flagData = parseToolResponse(result) - expect(flagData.id).toBeDefined() + expect(flagData.id).toBeTruthy() expect(flagData.key).toBe(params.key) expect(flagData.name).toBe(params.name) @@ -109,7 +110,7 @@ describe('Feature Flags', { concurrent: false }, () => { const result = await createTool.handler(context, params) const flagData = parseToolResponse(result) - expect(flagData.id).toBeDefined() + expect(flagData.id).toBeTruthy() expect(flagData.key).toBe(params.key) expect(flagData.name).toBe(params.name) @@ -186,7 +187,7 @@ describe('Feature Flags', { concurrent: false }, () => { const updateResult = await updateTool.handler(context, updateParams) const updatedFlag = parseToolResponse(updateResult) - expect(updatedFlag.id).toBeDefined() + expect(updatedFlag.id).toBeTruthy() expect(updatedFlag.key).toBe(createParams.key) }) }) @@ -223,7 +224,7 @@ describe('Feature Flags', { concurrent: false }, () => { // Verify our test flags are in the list for (const testFlag of testFlags) { const found = allFlags.find((f: any) => f.id === testFlag.id) - expect(found).toBeDefined() + expect(found).toBeTruthy() expect(found.key).toBe(testFlag.key) } }) @@ -276,9 +277,7 @@ describe('Feature Flags', { concurrent: false }, () => { const result = await getDefinitionTool.handler(context, { flagKey: nonExistentKey }) - expect(result.content[0].text).toBe( - `Error: Flag with key "${nonExistentKey}" not found.` - ) + expect(result.content[0].text).toBe(`Error: Flag with key "${nonExistentKey}" not found.`) }) }) @@ -301,7 +300,7 @@ describe('Feature Flags', { concurrent: false }, () => { // Delete the flag const deleteResult = await deleteTool.handler(context, { flagKey: createParams.key }) - expect(deleteResult.content).toBeDefined() + expect(deleteResult.content).toBeTruthy() expect(deleteResult.content[0].type).toBe('text') const deleteResponse = parseToolResponse(deleteResult) expect(deleteResponse.success).toBe(true) @@ -312,9 +311,7 @@ describe('Feature Flags', { concurrent: false }, () => { const getResult = await getDefinitionTool.handler(context, { flagKey: createParams.key, }) - expect(getResult.content[0].text).toBe( - `Error: Flag with key "${createParams.key}" not found.` - ) + expect(getResult.content[0].text).toBe(`Error: Flag with key "${createParams.key}" not found.`) }) it('should handle deletion of non-existent flag', async () => { diff --git a/products/mcp/typescript/tests/tools/insights.integration.test.ts b/products/mcp/typescript/tests/tools/insights.integration.test.ts index 30d363212f..5bd87c1025 100644 --- a/products/mcp/typescript/tests/tools/insights.integration.test.ts +++ b/products/mcp/typescript/tests/tools/insights.integration.test.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { type CreatedResources, SAMPLE_HOGQL_QUERIES, @@ -18,7 +20,6 @@ import getAllInsightsTool from '@/tools/insights/getAll' import queryInsightTool from '@/tools/insights/query' import updateInsightTool from '@/tools/insights/update' import type { Context } from '@/tools/types' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' describe('Insights', { concurrent: false }, () => { // All tests run sequentially to avoid conflicts with shared PostHog project @@ -57,7 +58,7 @@ describe('Insights', { concurrent: false }, () => { const result = await createTool.handler(context, params) const insightData = parseToolResponse(result) - expect(insightData.id).toBeDefined() + expect(insightData.id).toBeTruthy() expect(insightData.name).toBe(params.data.name) expect(insightData.url).toContain('/insights/') @@ -77,7 +78,7 @@ describe('Insights', { concurrent: false }, () => { const result = await createTool.handler(context, params) const insightData = parseToolResponse(result) - expect(insightData.id).toBeDefined() + expect(insightData.id).toBeTruthy() expect(insightData.name).toBe(params.data.name) createdResources.insights.push(insightData.id) @@ -97,7 +98,7 @@ describe('Insights', { concurrent: false }, () => { const result = await createTool.handler(context, params) const insightData = parseToolResponse(result) - expect(insightData.id).toBeDefined() + expect(insightData.id).toBeTruthy() expect(insightData.name).toBe(params.data.name) createdResources.insights.push(insightData.id) diff --git a/products/mcp/typescript/tests/tools/llmAnalytics.integration.test.ts b/products/mcp/typescript/tests/tools/llmAnalytics.integration.test.ts index 6f82fd4dd1..fa3510b8f2 100644 --- a/products/mcp/typescript/tests/tools/llmAnalytics.integration.test.ts +++ b/products/mcp/typescript/tests/tools/llmAnalytics.integration.test.ts @@ -1,14 +1,15 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { - validateEnvironmentVariables, + type CreatedResources, + TEST_ORG_ID, + TEST_PROJECT_ID, + cleanupResources, createTestClient, createTestContext, - setActiveProjectAndOrg, - cleanupResources, - TEST_PROJECT_ID, - TEST_ORG_ID, - type CreatedResources, parseToolResponse, + setActiveProjectAndOrg, + validateEnvironmentVariables, } from '@/shared/test-utils' import getLLMCostsTool from '@/tools/llmAnalytics/getLLMCosts' import type { Context } from '@/tools/types' diff --git a/products/mcp/typescript/tests/tools/organizations.integration.test.ts b/products/mcp/typescript/tests/tools/organizations.integration.test.ts index 0f57dc513a..63464c69d5 100644 --- a/products/mcp/typescript/tests/tools/organizations.integration.test.ts +++ b/products/mcp/typescript/tests/tools/organizations.integration.test.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { type CreatedResources, TEST_ORG_ID, @@ -13,7 +15,6 @@ import getOrganizationDetailsTool from '@/tools/organizations/getDetails' import getOrganizationsTool from '@/tools/organizations/getOrganizations' import setActiveOrganizationTool from '@/tools/organizations/setActive' import type { Context } from '@/tools/types' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' describe.skip('Organizations', { concurrent: false }, () => { let context: Context @@ -55,7 +56,7 @@ describe.skip('Organizations', { concurrent: false }, () => { const orgs = parseToolResponse(result) const testOrg = orgs.find((org: any) => org.id === TEST_ORG_ID) - expect(testOrg).toBeDefined() + expect(testOrg).toBeTruthy() expect(testOrg.id).toBe(TEST_ORG_ID) }) }) @@ -80,7 +81,7 @@ describe.skip('Organizations', { concurrent: false }, () => { await setTool.handler(context, { orgId: 'invalid-org-id-12345' }) expect.fail('Should have thrown an error') } catch (error) { - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) @@ -102,7 +103,7 @@ describe.skip('Organizations', { concurrent: false }, () => { const result = await getDetailsTool.handler(context, {}) const orgDetails = parseToolResponse(result) - expect(orgDetails.projects).toBeDefined() + expect(orgDetails.projects).toBeTruthy() expect(Array.isArray(orgDetails.projects)).toBe(true) if (orgDetails.projects.length > 0) { @@ -111,10 +112,8 @@ describe.skip('Organizations', { concurrent: false }, () => { expect(project).toHaveProperty('name') } - const testProject = orgDetails.projects.find( - (p: any) => p.id === Number(TEST_PROJECT_ID) - ) - expect(testProject).toBeDefined() + const testProject = orgDetails.projects.find((p: any) => p.id === Number(TEST_PROJECT_ID)) + expect(testProject).toBeTruthy() }) }) diff --git a/products/mcp/typescript/tests/tools/projects.integration.test.ts b/products/mcp/typescript/tests/tools/projects.integration.test.ts index 78dcdeb11a..5c2ac1def2 100644 --- a/products/mcp/typescript/tests/tools/projects.integration.test.ts +++ b/products/mcp/typescript/tests/tools/projects.integration.test.ts @@ -1,3 +1,6 @@ +import { v4 as uuidv4 } from 'uuid' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { type CreatedResources, TEST_ORG_ID, @@ -14,8 +17,6 @@ import getProjectsTool from '@/tools/projects/getProjects' import propertyDefinitionsTool from '@/tools/projects/propertyDefinitions' import setActiveProjectTool from '@/tools/projects/setActive' import type { Context } from '@/tools/types' -import { v4 as uuidv4 } from 'uuid' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' describe('Projects', { concurrent: false }, () => { let context: Context @@ -57,7 +58,7 @@ describe('Projects', { concurrent: false }, () => { const projects = parseToolResponse(result) const testProject = projects.find((proj: any) => proj.id === Number(TEST_PROJECT_ID)) - expect(testProject).toBeDefined() + expect(testProject).toBeTruthy() expect(testProject.id).toBe(Number(TEST_PROJECT_ID)) }) }) @@ -99,9 +100,7 @@ describe('Projects', { concurrent: false }, () => { expect(prop).toHaveProperty('property_type') expect(typeof prop.name).toBe('string') // property_type can be a string or null - expect(['string', 'object', 'undefined'].includes(typeof prop.property_type)).toBe( - true - ) + expect(['string', 'object', 'undefined'].includes(typeof prop.property_type)).toBe(true) } }) @@ -156,7 +155,7 @@ describe('Projects', { concurrent: false }, () => { const pageviewEvent = eventDefs.find((event: any) => event.name === '$pageview') if (eventDefs.length > 0) { - expect(pageviewEvent).toBeDefined() + expect(pageviewEvent).toBeTruthy() } }) @@ -206,8 +205,7 @@ describe('Projects', { concurrent: false }, () => { const projects = parseToolResponse(projectsResult) expect(projects.length).toBeGreaterThan(0) - const targetProject = - projects.find((p: any) => p.id === Number(TEST_PROJECT_ID)) || projects[0] + const targetProject = projects.find((p: any) => p.id === Number(TEST_PROJECT_ID)) || projects[0] const setResult = await setTool.handler(context, { projectId: targetProject.id }) expect(setResult.content[0].text).toBe(`Switched to project ${targetProject.id}`) diff --git a/products/mcp/typescript/tests/tools/query.integration.test.ts b/products/mcp/typescript/tests/tools/query.integration.test.ts index 776925a9f3..740065f832 100644 --- a/products/mcp/typescript/tests/tools/query.integration.test.ts +++ b/products/mcp/typescript/tests/tools/query.integration.test.ts @@ -1,8 +1,10 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import type { ApiClient } from '@/api/client' import type { InsightQuery } from '@/schema/query' import queryRunTool from '@/tools/query/run' import type { Context } from '@/tools/types' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { type CreatedResources, SAMPLE_FUNNEL_QUERIES, @@ -56,9 +58,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -70,9 +72,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -98,7 +100,7 @@ describe('Query Integration Tests', () => { query: invalidQuery, }) } catch (error: any) { - expect(error).toBeDefined() + expect(error).toBeTruthy() expect(error.message).toContain('Failed to query insight') } }) @@ -113,9 +115,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -127,9 +129,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -141,9 +143,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -155,9 +157,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -169,9 +171,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) }) @@ -185,9 +187,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -199,9 +201,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -213,9 +215,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -227,9 +229,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -241,9 +243,9 @@ describe('Query Integration Tests', () => { const response = parseToolResponse(result) - expect(result.content).toBeDefined() + expect(result.content).toBeTruthy() expect(result.content[0].type).toBe('text') - expect(response).toBeDefined() + expect(response).toBeTruthy() expect(Array.isArray(response)).toBe(true) }) @@ -275,7 +277,7 @@ describe('Query Integration Tests', () => { query: malformedFunnel as unknown as InsightQuery, }) } catch (error: any) { - expect(error).toBeDefined() + expect(error).toBeTruthy() } }) }) diff --git a/products/mcp/typescript/tests/tools/surveys.integration.test.ts b/products/mcp/typescript/tests/tools/surveys.integration.test.ts index baa3946d2f..79a45a0a6d 100644 --- a/products/mcp/typescript/tests/tools/surveys.integration.test.ts +++ b/products/mcp/typescript/tests/tools/surveys.integration.test.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + import { type CreatedResources, TEST_ORG_ID, @@ -18,7 +20,6 @@ import getGlobalSurveyStatsTool from '@/tools/surveys/global-stats' import getSurveyStatsTool from '@/tools/surveys/stats' import updateSurveyTool from '@/tools/surveys/update' import type { Context } from '@/tools/types' -import { afterEach, beforeAll, describe, expect, it } from 'vitest' describe('Surveys', { concurrent: false }, () => { let context: Context @@ -60,7 +61,7 @@ describe('Surveys', { concurrent: false }, () => { const result = await createTool.handler(context, params) const createResponse = parseToolResponse(result) - expect(createResponse.id).toBeDefined() + expect(createResponse.id).toBeTruthy() createdResources.surveys.push(createResponse.id) // Verify by getting the created survey @@ -103,12 +104,7 @@ describe('Surveys', { concurrent: false }, () => { { type: 'multiple_choice' as const, question: 'What improvements would you like to see?', - choices: [ - 'Better UI', - 'More integrations', - 'Faster performance', - 'Better docs', - ], + choices: ['Better UI', 'More integrations', 'Faster performance', 'Better docs'], hasOpenChoice: true, }, ], @@ -117,7 +113,7 @@ describe('Surveys', { concurrent: false }, () => { const result = await createTool.handler(context, params) const createResponse = parseToolResponse(result) - expect(createResponse.id).toBeDefined() + expect(createResponse.id).toBeTruthy() createdResources.surveys.push(createResponse.id) // Verify by getting the created survey @@ -142,8 +138,7 @@ describe('Surveys', { concurrent: false }, () => { questions: [ { type: 'rating' as const, - question: - 'How likely are you to recommend our product to a friend or colleague?', + question: 'How likely are you to recommend our product to a friend or colleague?', scale: 10 as const, display: 'number' as const, lowerBoundLabel: 'Not at all likely', @@ -175,7 +170,7 @@ describe('Surveys', { concurrent: false }, () => { const result = await createTool.handler(context, params) const createResponse = parseToolResponse(result) - expect(createResponse.id).toBeDefined() + expect(createResponse.id).toBeTruthy() createdResources.surveys.push(createResponse.id) // Verify by getting the created survey @@ -185,7 +180,7 @@ describe('Surveys', { concurrent: false }, () => { expect(surveyData.id).toBe(createResponse.id) expect(surveyData.name).toBe(params.name) expect(surveyData.questions).toHaveLength(4) - expect(surveyData.questions[0].branching).toBeDefined() + expect(surveyData.questions[0].branching).toBeTruthy() expect(surveyData.questions[0].branching.type).toBe('response_based') }) @@ -221,7 +216,7 @@ describe('Surveys', { concurrent: false }, () => { const result = await createTool.handler(context, params) const createResponse = parseToolResponse(result) - expect(createResponse.id).toBeDefined() + expect(createResponse.id).toBeTruthy() createdResources.surveys.push(createResponse.id) // Verify by getting the created survey @@ -230,7 +225,7 @@ describe('Surveys', { concurrent: false }, () => { expect(surveyData.id).toBe(createResponse.id) expect(surveyData.name).toBe(params.name) - expect(surveyData.targeting_flag).toBeDefined() + expect(surveyData.targeting_flag).toBeTruthy() }) }) @@ -270,9 +265,7 @@ describe('Surveys', { concurrent: false }, () => { it('should return error for non-existent survey ID', async () => { const nonExistentId = generateUniqueKey('non-existent') - await expect(getTool.handler(context, { surveyId: nonExistentId })).rejects.toThrow( - 'Failed to get survey' - ) + await expect(getTool.handler(context, { surveyId: nonExistentId })).rejects.toThrow('Failed to get survey') }) }) @@ -314,7 +307,7 @@ describe('Surveys', { concurrent: false }, () => { // Verify our test surveys are in the list for (const testSurvey of testSurveys) { const found = allSurveys.results.find((s: any) => s.id === testSurvey.id) - expect(found).toBeDefined() + expect(found).toBeTruthy() expect(found.name).toBe(testSurvey.name) } }) @@ -353,11 +346,11 @@ describe('Surveys', { concurrent: false }, () => { if (searchResults.results.length > 0) { const found = searchResults.results.find((s: any) => s.id === createdSurvey.id) // Survey might not be found in search results immediately - expect(found).toBeDefined() + expect(found).toBeTruthy() } } else { // If no results structure, just verify we got a response - expect(searchResults).toBeDefined() + expect(searchResults).toBeTruthy() } }) }) @@ -391,7 +384,7 @@ describe('Surveys', { concurrent: false }, () => { const statsResult = await statsTool.handler(context, { survey_id: createdSurvey.id }) const stats = parseToolResponse(statsResult) - expect(stats).toBeDefined() + expect(stats).toBeTruthy() // Stats may be undefined if no survey events exist yet expect(typeof (stats.survey_shown || 0)).toBe('number') expect(typeof (stats.survey_dismissed || 0)).toBe('number') @@ -406,7 +399,7 @@ describe('Surveys', { concurrent: false }, () => { const result = await globalStatsTool.handler(context, {}) const stats = parseToolResponse(result) - expect(stats).toBeDefined() + expect(stats).toBeTruthy() // Stats may be undefined if no survey events exist yet expect(typeof (stats.survey_shown || 0)).toBe('number') expect(typeof (stats.survey_dismissed || 0)).toBe('number') @@ -420,7 +413,7 @@ describe('Surveys', { concurrent: false }, () => { }) const stats = parseToolResponse(result) - expect(stats).toBeDefined() + expect(stats).toBeTruthy() }) }) @@ -451,7 +444,7 @@ describe('Surveys', { concurrent: false }, () => { // Delete the survey const deleteResult = await deleteTool.handler(context, { surveyId: createdSurvey.id }) - expect(deleteResult.content).toBeDefined() + expect(deleteResult.content).toBeTruthy() expect(deleteResult.content[0].type).toBe('text') const deleteResponse = parseToolResponse(deleteResult) expect(deleteResponse.success).toBe(true) @@ -711,9 +704,7 @@ describe('Surveys', { concurrent: false }, () => { } else { // If linked_flag_id is not set, the feature flag linking might not be supported // or there might be an issue with the update tool - console.warn( - 'Feature flag linking appears to not be working - linked_flag_id is undefined' - ) + console.warn('Feature flag linking appears to not be working - linked_flag_id is undefined') expect(updatedSurvey.id).toBe(createdSurvey.id) // At least verify the survey exists } }) @@ -771,7 +762,7 @@ describe('Surveys', { concurrent: false }, () => { const getResult = await getTool.handler(context, { surveyId: createdSurvey.id }) const updatedSurvey = parseToolResponse(getResult) - expect(updatedSurvey.targeting_flag).toBeDefined() + expect(updatedSurvey.targeting_flag).toBeTruthy() expect(updatedSurvey.targeting_flag.filters.groups).toHaveLength(1) expect(updatedSurvey.targeting_flag.filters.groups[0].properties).toHaveLength(2) expect(updatedSurvey.targeting_flag.filters.groups[0].rollout_percentage).toBe(75) @@ -821,7 +812,7 @@ describe('Surveys', { concurrent: false }, () => { expect(updatedSurvey.iteration_count).toBe(3) expect(updatedSurvey.iteration_frequency_days).toBe(7) expect(updatedSurvey.responses_limit).toBe(100) - expect(updatedSurvey.start_date).toBeDefined() + expect(updatedSurvey.start_date).toBeTruthy() }) }) @@ -873,7 +864,7 @@ describe('Surveys', { concurrent: false }, () => { const updateResult = await updateTool.handler(context, updateParams) const updateResponse = parseToolResponse(updateResult) - expect(updateResponse.id).toBeDefined() + expect(updateResponse.id).toBeTruthy() // Verify update by getting the survey const getUpdatedResult = await getTool.handler(context, { surveyId: createdSurvey.id }) @@ -951,7 +942,7 @@ describe('Surveys', { concurrent: false }, () => { const createResult = await createTool.handler(context, createParams) const createResponse = parseToolResponse(createResult) - expect(createResponse.id).toBeDefined() + expect(createResponse.id).toBeTruthy() createdResources.surveys.push(createResponse.id) // Verify creation by getting the survey @@ -960,8 +951,8 @@ describe('Surveys', { concurrent: false }, () => { expect(createdSurvey.id).toBe(createResponse.id) expect(createdSurvey.questions).toHaveLength(3) - expect(createdSurvey.questions[0]?.branching).toBeDefined() - expect(createdSurvey.targeting_flag).toBeDefined() + expect(createdSurvey.questions[0]?.branching).toBeTruthy() + expect(createdSurvey.targeting_flag).toBeTruthy() expect(createdSurvey.responses_limit).toBe(100) // Update survey @@ -970,7 +961,7 @@ describe('Surveys', { concurrent: false }, () => { responses_limit: 200, }) const updateResponse = parseToolResponse(updateResult) - expect(updateResponse.id).toBeDefined() + expect(updateResponse.id).toBeTruthy() // Verify update by getting the survey again const getUpdatedResult = await getTool.handler(context, { @@ -982,7 +973,7 @@ describe('Surveys', { concurrent: false }, () => { // Get stats const statsResult = await statsTool.handler(context, { survey_id: createdSurvey.id }) const stats = parseToolResponse(statsResult) - expect(stats).toBeDefined() + expect(stats).toBeTruthy() // Clean up const deleteResult = await deleteTool.handler(context, { surveyId: createdSurvey.id }) diff --git a/products/mcp/typescript/tests/unit/SessionManager.test.ts b/products/mcp/typescript/tests/unit/SessionManager.test.ts index 26a057c856..83efbf2254 100644 --- a/products/mcp/typescript/tests/unit/SessionManager.test.ts +++ b/products/mcp/typescript/tests/unit/SessionManager.test.ts @@ -1,7 +1,8 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + import { SessionManager } from '@/lib/utils/SessionManager' import type { ScopedCache } from '@/lib/utils/cache/ScopedCache' import type { State } from '@/tools' -import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('uuid', () => ({ v7: vi.fn(() => 'test-uuid-12345'), diff --git a/products/mcp/typescript/tests/unit/StateManager.test.ts b/products/mcp/typescript/tests/unit/StateManager.test.ts index 67d6d110ca..13fd0f2303 100644 --- a/products/mcp/typescript/tests/unit/StateManager.test.ts +++ b/products/mcp/typescript/tests/unit/StateManager.test.ts @@ -1,9 +1,10 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + import type { ApiClient } from '@/api/client' import { StateManager } from '@/lib/utils/StateManager' import { MemoryCache } from '@/lib/utils/cache/MemoryCache' import type { ApiRedactedPersonalApiKey, ApiUser } from '@/schema/api' import type { State } from '@/tools/types' -import { beforeEach, describe, expect, it, vi } from 'vitest' describe('StateManager', () => { let stateManager: StateManager @@ -29,9 +30,7 @@ describe('StateManager', () => { describe('getUser', () => { it('should fetch and cache user on first call', async () => { - const fetchUserSpy = vi - .spyOn(stateManager as any, '_fetchUser') - .mockResolvedValue(mockUser) + const fetchUserSpy = vi.spyOn(stateManager as any, '_fetchUser').mockResolvedValue(mockUser) const result = await stateManager.getUser() @@ -40,9 +39,7 @@ describe('StateManager', () => { }) it('should return cached user on subsequent calls', async () => { - const fetchUserSpy = vi - .spyOn(stateManager as any, '_fetchUser') - .mockResolvedValue(mockUser) + const fetchUserSpy = vi.spyOn(stateManager as any, '_fetchUser').mockResolvedValue(mockUser) await stateManager.getUser() const result = await stateManager.getUser() @@ -228,12 +225,10 @@ describe('StateManager', () => { }) it('should call setDefaultOrganizationAndProject when not cached', async () => { - const spy = vi - .spyOn(stateManager, 'setDefaultOrganizationAndProject') - .mockResolvedValue({ - organizationId: 'default-org', - projectId: 123, - }) + const spy = vi.spyOn(stateManager, 'setDefaultOrganizationAndProject').mockResolvedValue({ + organizationId: 'default-org', + projectId: 123, + }) const result = await stateManager.getOrgID() @@ -252,12 +247,10 @@ describe('StateManager', () => { }) it('should call setDefaultOrganizationAndProject when not cached', async () => { - const spy = vi - .spyOn(stateManager, 'setDefaultOrganizationAndProject') - .mockResolvedValue({ - organizationId: 'default-org', - projectId: 789, - }) + const spy = vi.spyOn(stateManager, 'setDefaultOrganizationAndProject').mockResolvedValue({ + organizationId: 'default-org', + projectId: 789, + }) const result = await stateManager.getProjectId() diff --git a/products/mcp/typescript/tests/unit/api-client.test.ts b/products/mcp/typescript/tests/unit/api-client.test.ts index ea5597b54d..7c26c2e05d 100644 --- a/products/mcp/typescript/tests/unit/api-client.test.ts +++ b/products/mcp/typescript/tests/unit/api-client.test.ts @@ -1,6 +1,7 @@ -import { ApiClient } from '@/api/client' import { describe, expect, it } from 'vitest' +import { ApiClient } from '@/api/client' + describe('ApiClient', () => { it('should create ApiClient with required config', () => { const client = new ApiClient({ diff --git a/products/mcp/typescript/tests/unit/integration-resources.test.ts b/products/mcp/typescript/tests/unit/integration-resources.test.ts index a70ad26edf..62c532a573 100644 --- a/products/mcp/typescript/tests/unit/integration-resources.test.ts +++ b/products/mcp/typescript/tests/unit/integration-resources.test.ts @@ -1,13 +1,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { describe, expect, it, beforeEach } from 'vitest' -import { registerIntegrationResources } from '@/resources/integration' -import { ResourceUri, WORKFLOW_NEXT_STEP_MESSAGE } from '@/resources/integration/index' -import { getSupportedFrameworks } from '@/resources/integration/framework-mappings' -import type { Context } from '@/tools/types' +import { beforeEach, describe, expect, it } from 'vitest' +import { registerIntegrationResources } from '@/resources/integration' +import { getSupportedFrameworks } from '@/resources/integration/framework-mappings' +import { ResourceUri, WORKFLOW_NEXT_STEP_MESSAGE } from '@/resources/integration/index' import workflowBegin from '@/resources/integration/workflow-guides/1.0-event-setup-begin.md' import workflowEdit from '@/resources/integration/workflow-guides/1.1-event-setup-edit.md' import workflowRevise from '@/resources/integration/workflow-guides/1.2-event-setup-revise.md' +import type { Context } from '@/tools/types' const FRAMEWORK_TEMPLATE_VARIABLE = '{framework}' @@ -37,7 +37,7 @@ describe('Integration Resources - Workflow Sequence', () => { it('should append next step URI to first workflow', async () => { const resources = (server as any)._registeredResources const resource = resources[ResourceUri.WORKFLOW_SETUP_BEGIN] - expect(resource).toBeDefined() + expect(resource).toBeTruthy() const result = await resource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_BEGIN)) const content = result.contents[0].text @@ -50,7 +50,7 @@ describe('Integration Resources - Workflow Sequence', () => { it('should append next step URI to middle workflow', async () => { const resources = (server as any)._registeredResources const resource = resources[ResourceUri.WORKFLOW_SETUP_EDIT] - expect(resource).toBeDefined() + expect(resource).toBeTruthy() const result = await resource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_EDIT)) const content = result.contents[0].text @@ -63,7 +63,7 @@ describe('Integration Resources - Workflow Sequence', () => { it('should not append next step URI to last workflow', async () => { const resources = (server as any)._registeredResources const resource = resources[ResourceUri.WORKFLOW_SETUP_REVISE] - expect(resource).toBeDefined() + expect(resource).toBeTruthy() const result = await resource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_REVISE)) const content = result.contents[0].text @@ -78,13 +78,9 @@ describe('Integration Resources - Workflow Sequence', () => { const editResource = resources[ResourceUri.WORKFLOW_SETUP_EDIT] const reviseResource = resources[ResourceUri.WORKFLOW_SETUP_REVISE] - const beginResult = await beginResource.readCallback( - new URL(ResourceUri.WORKFLOW_SETUP_BEGIN) - ) + const beginResult = await beginResource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_BEGIN)) const editResult = await editResource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_EDIT)) - const reviseResult = await reviseResource.readCallback( - new URL(ResourceUri.WORKFLOW_SETUP_REVISE) - ) + const reviseResult = await reviseResource.readCallback(new URL(ResourceUri.WORKFLOW_SETUP_REVISE)) expect(beginResult.contents[0].text).toContain(workflowBegin) expect(editResult.contents[0].text).toContain(workflowEdit) @@ -113,10 +109,10 @@ describe('Integration Resources - Resource Templates', () => { .toString() .startsWith(ResourceUri.DOCS_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, '')) ) as any - expect(template).toBeDefined() + expect(template).toBeTruthy() const completeCallback = template.resourceTemplate.completeCallback('framework') - expect(completeCallback).toBeDefined() + expect(completeCallback).toBeTruthy() const frameworks = await completeCallback('') const expectedFrameworks = getSupportedFrameworks() @@ -130,17 +126,12 @@ describe('Integration Resources - Resource Templates', () => { const template = templates.find((t: any) => t.resourceTemplate.uriTemplate .toString() - .startsWith( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace( - FRAMEWORK_TEMPLATE_VARIABLE, - '' - ) - ) + .startsWith(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, '')) ) as any - expect(template).toBeDefined() + expect(template).toBeTruthy() const completeCallback = template.resourceTemplate.completeCallback('framework') - expect(completeCallback).toBeDefined() + expect(completeCallback).toBeTruthy() const frameworks = await completeCallback('') const expectedFrameworks = getSupportedFrameworks() @@ -158,10 +149,10 @@ describe('Integration Resources - Resource Templates', () => { .toString() .startsWith(ResourceUri.DOCS_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, '')) ) as any - expect(template).toBeDefined() + expect(template).toBeTruthy() const listCallback = template.resourceTemplate.listCallback - expect(listCallback).toBeDefined() + expect(listCallback).toBeTruthy() const result = await listCallback({}) const frameworks = getSupportedFrameworks() @@ -169,12 +160,9 @@ describe('Integration Resources - Resource Templates', () => { expect(result.resources).toHaveLength(frameworks.length) for (const framework of frameworks) { - const expectedUri = ResourceUri.DOCS_FRAMEWORK.replace( - FRAMEWORK_TEMPLATE_VARIABLE, - framework - ) + const expectedUri = ResourceUri.DOCS_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, framework) const resource = result.resources.find((r: any) => r.uri === expectedUri) - expect(resource).toBeDefined() + expect(resource).toBeTruthy() } }) @@ -183,17 +171,12 @@ describe('Integration Resources - Resource Templates', () => { const template = templates.find((t: any) => t.resourceTemplate.uriTemplate .toString() - .startsWith( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace( - FRAMEWORK_TEMPLATE_VARIABLE, - '' - ) - ) + .startsWith(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, '')) ) as any - expect(template).toBeDefined() + expect(template).toBeTruthy() const listCallback = template.resourceTemplate.listCallback - expect(listCallback).toBeDefined() + expect(listCallback).toBeTruthy() const result = await listCallback({}) const frameworks = getSupportedFrameworks() @@ -206,7 +189,7 @@ describe('Integration Resources - Resource Templates', () => { framework ) const resource = result.resources.find((r: any) => r.uri === expectedUri) - expect(resource).toBeDefined() + expect(resource).toBeTruthy() } }) @@ -220,12 +203,7 @@ describe('Integration Resources - Resource Templates', () => { const examplesTemplate = templates.find((t: any) => t.resourceTemplate.uriTemplate .toString() - .startsWith( - ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace( - FRAMEWORK_TEMPLATE_VARIABLE, - '' - ) - ) + .startsWith(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, '')) ) as any const docsResult = await docsTemplate.resourceTemplate.listCallback({}) @@ -233,23 +211,16 @@ describe('Integration Resources - Resource Templates', () => { const frameworks = getSupportedFrameworks() for (const framework of frameworks) { - const expectedDocsUri = ResourceUri.DOCS_FRAMEWORK.replace( - FRAMEWORK_TEMPLATE_VARIABLE, - framework - ) - const docsResource = docsResult.resources.find( - (r: any) => r.uri === expectedDocsUri - ) - expect(docsResource).toBeDefined() + const expectedDocsUri = ResourceUri.DOCS_FRAMEWORK.replace(FRAMEWORK_TEMPLATE_VARIABLE, framework) + const docsResource = docsResult.resources.find((r: any) => r.uri === expectedDocsUri) + expect(docsResource).toBeTruthy() const expectedExamplesUri = ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace( FRAMEWORK_TEMPLATE_VARIABLE, framework ) - const examplesResource = examplesResult.resources.find( - (r: any) => r.uri === expectedExamplesUri - ) - expect(examplesResource).toBeDefined() + const examplesResource = examplesResult.resources.find((r: any) => r.uri === expectedExamplesUri) + expect(examplesResource).toBeTruthy() } }) }) diff --git a/products/mcp/typescript/tests/unit/tool-filtering.test.ts b/products/mcp/typescript/tests/unit/tool-filtering.test.ts index b38686a1f8..a17c0aa0ef 100644 --- a/products/mcp/typescript/tests/unit/tool-filtering.test.ts +++ b/products/mcp/typescript/tests/unit/tool-filtering.test.ts @@ -1,20 +1,16 @@ +import { describe, expect, it } from 'vitest' + import { SessionManager } from '@/lib/utils/SessionManager' import { getToolsFromContext } from '@/tools' import { getToolsForFeatures } from '@/tools/toolDefinitions' import type { Context } from '@/tools/types' -import { describe, expect, it } from 'vitest' describe('Tool Filtering - Features', () => { const featureTests = [ { features: undefined, description: 'all tools when no features specified', - expectedTools: [ - 'feature-flag-get-definition', - 'dashboard-create', - 'insights-get-all', - 'organizations-get', - ], + expectedTools: ['feature-flag-get-definition', 'dashboard-create', 'insights-get-all', 'organizations-get'], }, { features: [], @@ -160,11 +156,7 @@ describe('Tool Filtering - API Scopes', () => { }) it('should return multiple scope tools when user has multiple scopes', async () => { - const context = createMockContext([ - 'dashboard:read', - 'feature_flag:write', - 'organization:read', - ]) + const context = createMockContext(['dashboard:read', 'feature_flag:write', 'organization:read']) const tools = await getToolsFromContext(context) const toolNames = tools.map((t) => t.name)