From 4505f482cb1e75336a52edee48dd21c13eaf0847 Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Tue, 22 Jul 2025 14:39:16 +0200 Subject: [PATCH 1/4] feat: tools refactor --- src/index.ts | 881 +------------------- src/tools/dashboards/addInsight.ts | 51 ++ src/tools/dashboards/create.ts | 39 + src/tools/dashboards/delete.ts | 31 + src/tools/dashboards/get.ts | 33 + src/tools/dashboards/getAll.ts | 35 + src/tools/dashboards/update.ts | 42 + src/tools/documentation/searchDocs.ts | 39 + src/tools/errorTracking/errorDetails.ts | 48 ++ src/tools/errorTracking/listErrors.ts | 51 ++ src/tools/featureFlags/create.ts | 53 ++ src/tools/featureFlags/delete.ts | 51 ++ src/tools/featureFlags/getAll.ts | 28 + src/tools/featureFlags/getDefinition.ts | 83 ++ src/tools/featureFlags/update.ts | 49 ++ src/tools/index.ts | 92 ++ src/tools/insights/create.ts | 54 ++ src/tools/insights/delete.ts | 31 + src/tools/insights/get.ts | 36 + src/tools/insights/getAll.ts | 41 + src/tools/insights/getSqlInsight.ts | 51 ++ src/tools/insights/update.ts | 43 + src/tools/llmObservability/getLLMCosts.ts | 67 ++ src/tools/organizations/getDetails.ts | 33 + src/tools/organizations/getOrganizations.ts | 28 + src/tools/organizations/setActive.ts | 28 + src/tools/projects/getProjects.ts | 31 + src/tools/projects/propertyDefinitions.ts | 32 + src/tools/projects/setActive.ts | 29 + src/tools/types.ts | 27 + tests/api/client.test.ts | 4 +- 31 files changed, 1282 insertions(+), 859 deletions(-) create mode 100644 src/tools/dashboards/addInsight.ts create mode 100644 src/tools/dashboards/create.ts create mode 100644 src/tools/dashboards/delete.ts create mode 100644 src/tools/dashboards/get.ts create mode 100644 src/tools/dashboards/getAll.ts create mode 100644 src/tools/dashboards/update.ts create mode 100644 src/tools/documentation/searchDocs.ts create mode 100644 src/tools/errorTracking/errorDetails.ts create mode 100644 src/tools/errorTracking/listErrors.ts create mode 100644 src/tools/featureFlags/create.ts create mode 100644 src/tools/featureFlags/delete.ts create mode 100644 src/tools/featureFlags/getAll.ts create mode 100644 src/tools/featureFlags/getDefinition.ts create mode 100644 src/tools/featureFlags/update.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/insights/create.ts create mode 100644 src/tools/insights/delete.ts create mode 100644 src/tools/insights/get.ts create mode 100644 src/tools/insights/getAll.ts create mode 100644 src/tools/insights/getSqlInsight.ts create mode 100644 src/tools/insights/update.ts create mode 100644 src/tools/llmObservability/getLLMCosts.ts create mode 100644 src/tools/organizations/getDetails.ts create mode 100644 src/tools/organizations/getOrganizations.ts create mode 100644 src/tools/organizations/setActive.ts create mode 100644 src/tools/projects/getProjects.ts create mode 100644 src/tools/projects/propertyDefinitions.ts create mode 100644 src/tools/projects/setActive.ts create mode 100644 src/tools/types.ts diff --git a/src/index.ts b/src/index.ts index 010b32c..8139c74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,27 +3,12 @@ import { McpAgent } from "agents/mcp"; import { z } from "zod"; import { ApiClient } from "./api/client"; - -import { - AddInsightToDashboardSchema, - CreateDashboardInputSchema, - ListDashboardsSchema, - UpdateDashboardInputSchema, -} from "./schema/dashboards"; -import { FilterGroupsSchema, UpdateFeatureFlagInputSchema } from "./schema/flags"; -import { - CreateInsightInputSchema, - ListInsightsSchema, - UpdateInsightInputSchema, -} from "./schema/insights"; - -import { docsSearch } from "./inkeepApi"; import { getPostHogClient } from "./lib/client"; -import { getProjectBaseUrl } from "./lib/utils/api"; import { DurableObjectCache } from "./lib/utils/cache/DurableObjectCache"; import { handleToolError } from "./lib/utils/handleToolError"; import { hash } from "./lib/utils/helper-functions"; -import { ErrorDetailsSchema, ListErrorsSchema } from "./schema/errors"; +import tools from "./tools"; +import type { Context, State } from "./tools/types"; const INSTRUCTIONS = ` - You are a helpful assistant that can query PostHog API. @@ -36,11 +21,6 @@ type RequestProperties = { apiToken: string; }; -type State = { - projectId: string | undefined; - orgId: string | undefined; - distinctId: string | undefined; -}; // Define our MCP agent with tools export class MyMCP extends McpAgent { server = new McpServer({ @@ -175,843 +155,30 @@ export class MyMCP extends McpAgent { return projectId; } - async init() { - this.registerTool( - "feature-flag-get-definition", - ` - - Use this tool to get the definition of a feature flag. - - You can provide either the flagId or the flagKey. - - If you provide both, the flagId will be used. - `, - { - flagId: z.string().optional(), - flagKey: z.string().optional(), - }, - async ({ flagId, flagKey }) => { - if (!flagId && !flagKey) { - return { - content: [ - { - type: "text", - text: "Error: Either flagId or flagKey must be provided.", - }, - ], - }; - } - - const projectId = await this.getProjectId(); - if (flagId) { - const flagResult = await this.api - .featureFlags({ projectId }) - .get({ flagId: String(flagId) }); - if (!flagResult.success) { - throw new Error(`Failed to get feature flag: ${flagResult.error.message}`); - } - return { - content: [{ type: "text", text: JSON.stringify(flagResult.data) }], - }; - } - - if (flagKey) { - const flagResult = await this.api - .featureFlags({ projectId }) - .findByKey({ key: flagKey }); - if (!flagResult.success) { - throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); - } - if (flagResult.data) { - return { - content: [{ type: "text", text: JSON.stringify(flagResult.data) }], - }; - } - return { - content: [ - { - type: "text", - text: `Error: Flag with key "${flagKey}" not found.`, - }, - ], - }; - } - - return { - content: [ - { - type: "text", - text: "Error: Could not determine or find the feature flag.", - }, - ], - }; - }, - ); - - this.registerTool( - "feature-flag-get-all", - ` - - Use this tool to get all feature flags in the project. - `, - {}, - async () => { - const projectId = await this.getProjectId(); - - const flagsResult = await this.api.featureFlags({ projectId }).list(); - if (!flagsResult.success) { - throw new Error(`Failed to get feature flags: ${flagsResult.error.message}`); - } - - return { content: [{ type: "text", text: JSON.stringify(flagsResult.data) }] }; - }, - ); - - this.registerTool( - "docs-search", - ` - - Use this tool to search the PostHog documentation for information that can help the user with their request. - - Use it as a fallback when you cannot answer the user's request using other tools in this MCP. - `, - { - query: z.string(), - }, - async ({ query }) => { - const inkeepApiKey = this.env.INKEEP_API_KEY; - - if (!inkeepApiKey) { - return { - content: [ - { - type: "text", - text: "Error: INKEEP_API_KEY is not configured.", - }, - ], - }; - } - const resultText = await docsSearch(inkeepApiKey, query); - return { content: [{ type: "text", text: resultText }] }; - }, - ); - this.registerTool( - "organizations-get", - ` - - Use this tool to get the organizations the user has access to. - `, - {}, - async () => { - const orgsResult = await this.api.organizations().list(); - if (!orgsResult.success) { - throw new Error(`Failed to get organizations: ${orgsResult.error.message}`); - } - console.log("organizations", orgsResult.data); - return { - content: [{ type: "text", text: JSON.stringify(orgsResult.data) }], - }; - }, - ); - - this.registerTool( - "project-set-active", - ` - - Use this tool to set the active project. - `, - { - projectId: z.string(), - }, - async ({ projectId }) => { - await this.cache.set("projectId", projectId); - - return { - content: [{ type: "text", text: `Switched to project ${projectId}` }], - }; - }, - ); - - this.registerTool( - "organization-set-active", - ` - - Use this tool to set the active organization. - `, - { - orgId: z.string(), - }, - async ({ orgId }) => { - await this.cache.set("orgId", orgId); - - return { - content: [{ type: "text", text: `Switched to organization ${orgId}` }], - }; - }, - ); - - this.registerTool( - "organization-details-get", - ` - - Use this tool to get the details of the active organization. - `, - {}, - async () => { - const orgId = await this.getOrgID(); - - const orgResult = await this.api.organizations().get({ orgId }); - if (!orgResult.success) { - throw new Error( - `Failed to get organization details: ${orgResult.error.message}`, - ); - } - console.log("organization details", orgResult.data); - return { - content: [{ type: "text", text: JSON.stringify(orgResult.data) }], - }; - }, - ); - - this.registerTool( - "projects-get", - ` - - Fetches projects that the user has access to - the orgId is optional. - - Use this tool before you use any other tools (besides organization-* and docs-search) to allow user to select the project they want to use for subsequent requests. - `, - {}, - async () => { - const orgId = await this.getOrgID(); - const projectsResult = await this.api.organizations().projects({ orgId }).list(); - if (!projectsResult.success) { - throw new Error(`Failed to get projects: ${projectsResult.error.message}`); - } - console.log("projects", projectsResult.data); - return { - content: [{ type: "text", text: JSON.stringify(projectsResult.data) }], - }; - }, - ); - - this.registerTool( - "property-definitions", - ` - - Use this tool to get the property definitions of the active project. - `, - {}, - async () => { - const projectId = await this.getProjectId(); - - const propDefsResult = await this.api.projects().propertyDefinitions({ projectId }); - - if (!propDefsResult.success) { - throw new Error( - `Failed to get property definitions: ${propDefsResult.error.message}`, - ); - } - return { - content: [{ type: "text", text: JSON.stringify(propDefsResult.data) }], - }; - }, - ); - - this.registerTool( - "create-feature-flag", - `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. - `, - { - name: z.string(), - key: z.string(), - description: z.string(), - filters: FilterGroupsSchema, - active: z.boolean(), - tags: z.array(z.string()).optional(), - }, - async ({ name, key, description, filters, active, tags }) => { - const projectId = await this.getProjectId(); - - const flagResult = await this.api.featureFlags({ projectId }).create({ - data: { name, key, description, filters, active }, - }); - - if (!flagResult.success) { - throw new Error(`Failed to create feature flag: ${flagResult.error.message}`); - } - - // Add URL field for easy navigation - const featureFlagWithUrl = { - ...flagResult.data, - url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, - }; - - return { - content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], - }; - }, - ); - - this.registerTool( - "list-errors", - ` - - Use this tool to list errors in the project. - `, - { - data: ListErrorsSchema, - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - - const errorQuery = { - kind: "ErrorTrackingQuery", - orderBy: data.orderBy || "occurrences", - dateRange: { - date_from: - data.dateFrom?.toISOString() || - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), - date_to: data.dateTo?.toISOString() || new Date().toISOString(), - }, - volumeResolution: 1, - orderDirection: data.orderDirection || "DESC", - filterTestAccounts: data.filterTestAccounts ?? true, - status: data.status || "active", - }; - - const errorsResult = await this.api - .query({ projectId }) - .execute({ queryBody: errorQuery }); - if (!errorsResult.success) { - throw new Error(`Failed to list errors: ${errorsResult.error.message}`); - } - console.log("errors results", errorsResult.data.results); - return { - content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], - }; - }, - ); - - this.registerTool( - "error-details", - ` - - Use this tool to get the details of an error in the project. - `, - { - data: ErrorDetailsSchema, - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - - const errorQuery = { - kind: "ErrorTrackingQuery", - dateRange: { - date_from: - data.dateFrom?.toISOString() || - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), - date_to: data.dateTo?.toISOString() || new Date().toISOString(), - }, - volumeResolution: 0, - issueId: data.issueId, - }; - - const errorsResult = await this.api - .query({ projectId }) - .execute({ queryBody: errorQuery }); - if (!errorsResult.success) { - throw new Error(`Failed to get error details: ${errorsResult.error.message}`); - } - console.log("error details results", errorsResult.data.results); - return { - content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], - }; - }, - ); - - this.registerTool( - "update-feature-flag", - `Update a new feature flag in the project. - - To enable a feature flag, you should make sure it is active and the rollout percentage is set to 100 for the group you want to target. - - To disable a feature flag, you should make sure it is inactive, you can keep the rollout percentage as it is. - `, - { - flagKey: z.string(), - data: UpdateFeatureFlagInputSchema, - }, - async ({ flagKey, data }) => { - const projectId = await this.getProjectId(); - - const flagResult = await this.api.featureFlags({ projectId }).update({ - key: flagKey, - data: data, - }); - if (!flagResult.success) { - throw new Error(`Failed to update feature flag: ${flagResult.error.message}`); - } - - // Add URL field for easy navigation - const featureFlagWithUrl = { - ...flagResult.data, - url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, - }; - - return { - content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], - }; - }, - ); - - this.registerTool( - "delete-feature-flag", - ` - - Use this tool to delete a feature flag in the project. - `, - { - flagKey: z.string(), - }, - async ({ flagKey }) => { - const projectId = await this.getProjectId(); - - const flagResult = await this.api - .featureFlags({ projectId }) - .findByKey({ key: flagKey }); - if (!flagResult.success) { - throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); - } - - if (!flagResult.data) { - return { - content: [{ type: "text", text: "Feature flag is already deleted." }], - }; - } - - const deleteResult = await this.api.featureFlags({ projectId }).delete({ - flagId: flagResult.data.id, - }); - if (!deleteResult.success) { - throw new Error(`Failed to delete feature flag: ${deleteResult.error.message}`); - } - - return { - content: [{ type: "text", text: JSON.stringify(deleteResult.data) }], - }; - }, - ); - - this.registerTool( - "get-sql-insight", - ` - - Queries project's PostHog data warehouse based on a provided natural language question - don't provide SQL query as input but describe the output you want. - - Data warehouse schema includes data like events and persons. - - Use this tool to get a quick answer to a question about the data in the project, which can't be answered using other, more dedicated tools. - - Fetches the result as a Server-Sent Events (SSE) stream and provides the concatenated data content. - - When giving the results back to the user, first show the SQL query that was used, then briefly explain the query, then provide results in reasily readable format. - - You should also offer to save the query as an insight if the user wants to. - `, - { - query: z - .string() - .max(1000) - .describe( - "Your natural language query describing the SQL insight (max 1000 characters).", - ), - }, - async ({ query }) => { - const apiToken = this.requestProperties.apiToken; - if (!apiToken) { - return { - content: [ - { - type: "text", - text: "Error: POSTHOG_API_TOKEN is not configured.", - }, - ], - }; - } - - const projectId = await this.getProjectId(); - - const result = await this.api.insights({ projectId }).sqlInsight({ query }); - if (!result.success) { - throw new Error(`Failed to execute SQL insight: ${result.error.message}`); - } - - if (result.data.results.length === 0) { - return { - content: [ - { - type: "text", - text: "Received an empty SQL insight or no data in the stream.", - }, - ], - }; - } - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; - }, - ); - - this.registerTool( - "get-llm-total-costs-for-project", - ` - - Fetches the total LLM daily costs for each model for a project over a given number of days. - - If no number of days is provided, it defaults to 7. - - The results are sorted by model name. - - The total cost is rounded to 4 decimal places. - - The query is executed against the project's data warehouse. - - Show the results as a Markdown formatted table with the following information for each model: - - Model name - - Total cost in USD - - Each day's date - - Each day's cost in USD - - Write in bold the model name with the highest total cost. - - Properly render the markdown table in the response. - `, - { - projectId: z.string(), - days: z.number().optional(), - }, - async ({ projectId, days }) => { - const trendsQuery = { - kind: "TrendsQuery", - dateRange: { - date_from: `-${days || 6}d`, - date_to: null, - }, - filterTestAccounts: true, - series: [ - { - event: "$ai_generation", - name: "$ai_generation", - math: "sum", - math_property: "$ai_total_cost_usd", - kind: "EventsNode", - }, - ], - breakdownFilter: { - breakdown_type: "event", - breakdown: "$ai_model", - }, - }; - - const costsResult = await this.api - .query({ projectId }) - .execute({ queryBody: trendsQuery }); - if (!costsResult.success) { - throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`); - } - return { - content: [{ type: "text", text: JSON.stringify(costsResult.data.results) }], - }; - }, - ); - - this.registerTool( - "insights-get-all", - ` - - Get all insights in the project with optional filtering. - - Can filter by saved status, favorited status, or search term. - `, - { - data: ListInsightsSchema.optional(), - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - const insightsResult = await this.api - .insights({ projectId }) - .list({ params: data }); - if (!insightsResult.success) { - throw new Error(`Failed to get insights: ${insightsResult.error.message}`); - } - - // Add URL field to each insight for easy navigation - const insightsWithUrls = insightsResult.data.map((insight) => ({ - ...insight, - url: `${getProjectBaseUrl(projectId)}/insights/${insight.short_id}`, - })); - - return { content: [{ type: "text", text: JSON.stringify(insightsWithUrls) }] }; - }, - ); - - this.registerTool( - "insight-get", - ` - - Get a specific insight by ID. - `, - { - insightId: z.number(), - }, - async ({ insightId }) => { - const projectId = await this.getProjectId(); - const insightResult = await this.api.insights({ projectId }).get({ insightId }); - if (!insightResult.success) { - throw new Error(`Failed to get insight: ${insightResult.error.message}`); - } - - // Add URL field for easy navigation - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; - }, - ); - - this.registerTool( - "insight-create-from-query", - ` - - You can use this to save a query as an insight. You should only do this with a valid query that you have seen, or one you have modified slightly. - - If the user wants to see data, you should use the "get-sql-insight" tool to get that data instead. - - An insight requires a name, query, and other optional properties. - - The query should use HogQL, which is a variant of Clickhouse SQL. Here is an example query: - Here is an example of a validquery: - { - "kind": "DataVisualizationNode", - "source": { - "kind": "HogQLQuery", - "query": "SELECT\n event,\n count() AS event_count\nFROM\n events\nWHERE\n timestamp >= now() - INTERVAL 7 day\nGROUP BY\n event\nORDER BY\n event_count DESC\nLIMIT 10", - "explain": true, - "filters": { - "dateRange": { - "date_from": "-7d" - } - } - }, - } - `, - { - data: CreateInsightInputSchema, - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - const insightResult = await this.api.insights({ projectId }).create({ data }); - if (!insightResult.success) { - throw new Error(`Failed to create insight: ${insightResult.error.message}`); - } - - // Add URL field for easy navigation - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; - }, - ); - - this.registerTool( - "insight-update", - ` - - Update an existing insight by ID. - - Can update name, description, filters, and other properties. - `, - { - insightId: z.number(), - data: UpdateInsightInputSchema, - }, - async ({ insightId, data }) => { - const projectId = await this.getProjectId(); - const insightResult = await this.api.insights({ projectId }).update({ - insightId, - data, - }); - - if (!insightResult.success) { - throw new Error(`Failed to update insight: ${insightResult.error.message}`); - } - - // Add URL field for easy navigation - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; - }, - ); - - this.registerTool( - "insight-delete", - ` - - Delete an insight by ID (soft delete - marks as deleted). - `, - { - insightId: z.number(), - }, - async ({ insightId }) => { - const projectId = await this.getProjectId(); - const result = await this.api.insights({ projectId }).delete({ insightId }); - - if (!result.success) { - throw new Error(`Failed to delete insight: ${result.error.message}`); - } - - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; - }, - ); - - // Dashboard tools - this.registerTool( - "dashboards-get-all", - ` - - Get all dashboards in the project with optional filtering. - - Can filter by pinned status, search term, or pagination. - `, - { - data: ListDashboardsSchema.optional(), - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - const dashboardsResult = await this.api - .dashboards({ projectId }) - .list({ params: data }); - - if (!dashboardsResult.success) { - throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`); - } - - return { content: [{ type: "text", text: JSON.stringify(dashboardsResult.data) }] }; - }, - ); - - this.registerTool( - "dashboard-get", - ` - - Get a specific dashboard by ID. - `, - { - dashboardId: z.number(), - }, - async ({ dashboardId }) => { - const projectId = await this.getProjectId(); - const dashboardResult = await this.api - .dashboards({ projectId }) - .get({ dashboardId }); - - if (!dashboardResult.success) { - throw new Error(`Failed to get dashboard: ${dashboardResult.error.message}`); - } - - return { content: [{ type: "text", text: JSON.stringify(dashboardResult.data) }] }; - }, - ); - - this.registerTool( - "dashboard-create", - ` - - Create a new dashboard in the project. - - Requires name and optional description, tags, and other properties. - `, - { - data: CreateDashboardInputSchema, - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - const dashboardResult = await this.api.dashboards({ projectId }).create({ data }); - - if (!dashboardResult.success) { - throw new Error(`Failed to create dashboard: ${dashboardResult.error.message}`); - } - - // Add URL field for easy navigation - const dashboardWithUrl = { - ...dashboardResult.data, - url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; - }, - ); - - this.registerTool( - "dashboard-update", - ` - - Update an existing dashboard by ID. - - Can update name, description, pinned status or tags. - `, - { - dashboardId: z.number(), - data: UpdateDashboardInputSchema, - }, - async ({ dashboardId, data }) => { - const projectId = await this.getProjectId(); - const dashboardResult = await this.api - .dashboards({ projectId }) - .update({ dashboardId, data }); - - if (!dashboardResult.success) { - throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`); - } - - // Add URL field for easy navigation - const dashboardWithUrl = { - ...dashboardResult.data, - url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; - }, - ); - - this.registerTool( - "dashboard-delete", - ` - - Delete a dashboard by ID (soft delete - marks as deleted). - `, - { - dashboardId: z.number(), - }, - async ({ dashboardId }) => { - const projectId = await this.getProjectId(); - const result = await this.api.dashboards({ projectId }).delete({ dashboardId }); - - if (!result.success) { - throw new Error(`Failed to delete dashboard: ${result.error.message}`); - } - - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; - }, - ); - - this.registerTool( - "add-insight-to-dashboard", - ` - - Add an existing insight to a dashboard. - - Requires insight ID and dashboard ID. - - Optionally supports layout and color customization. - `, - { - data: AddInsightToDashboardSchema, - }, - async ({ data }) => { - const projectId = await this.getProjectId(); - - // Get insight to retrieve short_id for URL - const insightResult = await this.api - .insights({ projectId }) - .get({ insightId: data.insight_id }); - if (!insightResult.success) { - throw new Error(`Failed to get insight: ${insightResult.error.message}`); - } - - const result = await this.api.dashboards({ projectId }).addInsight({ data }); - - if (!result.success) { - throw new Error(`Failed to add insight to dashboard: ${result.error.message}`); - } - - // Add URLs for easy navigation - const resultWithUrls = { - ...result.data, - dashboard_url: `${getProjectBaseUrl(projectId)}/dashboard/${data.dashboard_id}`, - insight_url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; + getContext(): Context { + return { + api: this.api, + cache: this.cache, + env: this.env, + getProjectId: this.getProjectId.bind(this), + getOrgID: this.getOrgID.bind(this), + getDistinctId: this.getDistinctId.bind(this), + }; + } - return { content: [{ type: "text", text: JSON.stringify(resultWithUrls) }] }; - }, - ); + async init() { + const context = this.getContext(); + const allTools = tools(context); + + for (const tool of allTools) { + this.registerTool( + tool.name, + tool.description, + tool.schema.shape, + async (params) => tool.handler(context, params), + ); + } - // this.server.prompt("add-feature-flag-to-codebase", "Use this prompt to add a feature flag to the codebase", async ({ - // }) => { - // return `Follow these steps to add a feature flag to the codebase: - // 1. Ask the user what flag they want to add if it is not already obvious. - // 2. Search for that flag, if it does not exist, create it. - // 3. Search the docs for the right language / framework on how to add a feature flag - make sure you get the docs you need. - // 4. Gather any context you need on how flags are used in the codebase. - // 5. Add the feature flag to the codebase. - // ` - // }) } } diff --git a/src/tools/dashboards/addInsight.ts b/src/tools/dashboards/addInsight.ts new file mode 100644 index 0000000..447164c --- /dev/null +++ b/src/tools/dashboards/addInsight.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { AddInsightToDashboardSchema } from "../../schema/dashboards"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + data: AddInsightToDashboardSchema, +}); + +type Params = z.infer; + +export const addInsightHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + + const insightResult = await context.api + .insights({ projectId }) + .get({ insightId: data.insight_id }); + + if (!insightResult.success) { + throw new Error(`Failed to get insight: ${insightResult.error.message}`); + } + + const result = await context.api.dashboards({ projectId }).addInsight({ data }); + + if (!result.success) { + throw new Error(`Failed to add insight to dashboard: ${result.error.message}`); + } + + + const resultWithUrls = { + ...result.data, + dashboard_url: `${getProjectBaseUrl(projectId)}/dashboard/${data.dashboard_id}`, + insight_url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(resultWithUrls) }] }; +}; + +const tool = (): Tool => ({ + name: "add-insight-to-dashboard", + description: ` + - Add an existing insight to a dashboard. + - Requires insight ID and dashboard ID. + - Optionally supports layout and color customization. + `, + schema, + handler: addInsightHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/dashboards/create.ts b/src/tools/dashboards/create.ts new file mode 100644 index 0000000..4af35c1 --- /dev/null +++ b/src/tools/dashboards/create.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { CreateDashboardInputSchema } from "../../schema/dashboards"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + data: CreateDashboardInputSchema, +}); + +type Params = z.infer; + +export const createHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api.dashboards({ projectId }).create({ data }); + + if (!dashboardResult.success) { + throw new Error(`Failed to create dashboard: ${dashboardResult.error.message}`); + } + + const dashboardWithUrl = { + ...dashboardResult.data, + url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; +}; + +const tool = (): Tool => ({ + name: "dashboard-create", + description: ` + - Create a new dashboard in the project. + - Requires name and optional description, tags, and other properties. + `, + schema, + handler: createHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/dashboards/delete.ts b/src/tools/dashboards/delete.ts new file mode 100644 index 0000000..5f85419 --- /dev/null +++ b/src/tools/dashboards/delete.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + dashboardId: z.number(), +}); + +type Params = z.infer; + +export const deleteHandler = async (context: Context, params: Params) => { + const { dashboardId } = params; + const projectId = await context.getProjectId(); + const result = await context.api.dashboards({ projectId }).delete({ dashboardId }); + + if (!result.success) { + throw new Error(`Failed to delete dashboard: ${result.error.message}`); + } + + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; +}; + +const tool = (): Tool => ({ + name: "dashboard-delete", + description: ` + - Delete a dashboard by ID (soft delete - marks as deleted). + `, + schema, + handler: deleteHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/dashboards/get.ts b/src/tools/dashboards/get.ts new file mode 100644 index 0000000..1f5cab3 --- /dev/null +++ b/src/tools/dashboards/get.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + dashboardId: z.number(), +}); + +type Params = z.infer; + +export const getHandler = async (context: Context, params: Params) => { + const { dashboardId } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api + .dashboards({ projectId }) + .get({ dashboardId }); + + if (!dashboardResult.success) { + throw new Error(`Failed to get dashboard: ${dashboardResult.error.message}`); + } + + return { content: [{ type: "text", text: JSON.stringify(dashboardResult.data) }] }; +}; + +const tool = (): Tool => ({ + name: "dashboard-get", + description: ` + - Get a specific dashboard by ID. + `, + schema, + handler: getHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/dashboards/getAll.ts b/src/tools/dashboards/getAll.ts new file mode 100644 index 0000000..55d0e97 --- /dev/null +++ b/src/tools/dashboards/getAll.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { ListDashboardsSchema } from "../../schema/dashboards"; + +const schema = z.object({ + data: ListDashboardsSchema.optional(), +}); + +type Params = z.infer; + +export const getAllHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + const dashboardsResult = await context.api + .dashboards({ projectId }) + .list({ params: data }); + + if (!dashboardsResult.success) { + throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`); + } + + return { content: [{ type: "text", text: JSON.stringify(dashboardsResult.data) }] }; +}; + +const tool = (): Tool => ({ + name: "dashboards-get-all", + description: ` + - Get all dashboards in the project with optional filtering. + - Can filter by pinned status, search term, or pagination. + `, + schema, + handler: getAllHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/dashboards/update.ts b/src/tools/dashboards/update.ts new file mode 100644 index 0000000..a4e4383 --- /dev/null +++ b/src/tools/dashboards/update.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { UpdateDashboardInputSchema } from "../../schema/dashboards"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + dashboardId: z.number(), + data: UpdateDashboardInputSchema, +}); + +type Params = z.infer; + +export const updateHandler = async (context: Context, params: Params) => { + const { dashboardId, data } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api + .dashboards({ projectId }) + .update({ dashboardId, data }); + + if (!dashboardResult.success) { + throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`); + } + + const dashboardWithUrl = { + ...dashboardResult.data, + url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; +}; + +const tool = (): Tool => ({ + name: "dashboard-update", + description: ` + - Update an existing dashboard by ID. + - Can update name, description, pinned status or tags. + `, + schema, + handler: updateHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/documentation/searchDocs.ts b/src/tools/documentation/searchDocs.ts new file mode 100644 index 0000000..68170e5 --- /dev/null +++ b/src/tools/documentation/searchDocs.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { docsSearch } from "../../inkeepApi"; + +const schema = z.object({ + query: z.string(), +}); + +type Params = z.infer; + +export const searchDocsHandler = async (context: Context, params: Params) => { + const { query } = params; + const inkeepApiKey = context.env.INKEEP_API_KEY; + + if (!inkeepApiKey) { + return { + content: [ + { + type: "text", + text: "Error: INKEEP_API_KEY is not configured.", + }, + ], + }; + } + const resultText = await docsSearch(inkeepApiKey, query); + return { content: [{ type: "text", text: resultText }] }; +}; + +const tool = (): Tool => ({ + name: "docs-search", + description: ` + - Use this tool to search the PostHog documentation for information that can help the user with their request. + - Use it as a fallback when you cannot answer the user's request using other tools in this MCP. + `, + schema, + handler: searchDocsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/errorTracking/errorDetails.ts b/src/tools/errorTracking/errorDetails.ts new file mode 100644 index 0000000..9121a3e --- /dev/null +++ b/src/tools/errorTracking/errorDetails.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { ErrorDetailsSchema } from "../../schema/errors"; + +const schema = z.object({ + data: ErrorDetailsSchema, +}); + +type Params = z.infer; + +export const errorDetailsHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + + const errorQuery = { + kind: "ErrorTrackingQuery", + dateRange: { + date_from: + data.dateFrom?.toISOString() || + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + date_to: data.dateTo?.toISOString() || new Date().toISOString(), + }, + volumeResolution: 0, + issueId: data.issueId, + }; + + const errorsResult = await context.api + .query({ projectId }) + .execute({ queryBody: errorQuery }); + if (!errorsResult.success) { + throw new Error(`Failed to get error details: ${errorsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], + }; +}; + +const tool = (): Tool => ({ + name: "error-details", + description: ` + - Use this tool to get the details of an error in the project. + `, + schema, + handler: errorDetailsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/errorTracking/listErrors.ts b/src/tools/errorTracking/listErrors.ts new file mode 100644 index 0000000..44635e6 --- /dev/null +++ b/src/tools/errorTracking/listErrors.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { ListErrorsSchema } from "../../schema/errors"; + +const schema = z.object({ + data: ListErrorsSchema, +}); + +type Params = z.infer; + +export const listErrorsHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + + const errorQuery = { + kind: "ErrorTrackingQuery", + orderBy: data.orderBy || "occurrences", + dateRange: { + date_from: + data.dateFrom?.toISOString() || + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + date_to: data.dateTo?.toISOString() || new Date().toISOString(), + }, + volumeResolution: 1, + orderDirection: data.orderDirection || "DESC", + filterTestAccounts: data.filterTestAccounts ?? true, + status: data.status || "active", + }; + + const errorsResult = await context.api + .query({ projectId }) + .execute({ queryBody: errorQuery }); + if (!errorsResult.success) { + throw new Error(`Failed to list errors: ${errorsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], + }; +}; + +const tool = (): Tool => ({ + name: "list-errors", + description: ` + - Use this tool to list errors in the project. + `, + schema, + handler: listErrorsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/featureFlags/create.ts b/src/tools/featureFlags/create.ts new file mode 100644 index 0000000..6d83ef4 --- /dev/null +++ b/src/tools/featureFlags/create.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { FilterGroupsSchema } from "../../schema/flags"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + name: z.string(), + key: z.string(), + description: z.string(), + filters: FilterGroupsSchema, + active: z.boolean(), + tags: z.array(z.string()).optional(), +}); + +type Params = z.infer; + +export const createHandler = async ( + context: Context, + params: Params +) => { + const { name, key, description, filters, active, tags } = params; + const projectId = await context.getProjectId(); + + 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}`); + } + + const featureFlagWithUrl = { + ...flagResult.data, + url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, + }; + + return { + content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], + }; +}; + +const tool = (): Tool => ({ + name: "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. + `, + schema, + handler: createHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/featureFlags/delete.ts b/src/tools/featureFlags/delete.ts new file mode 100644 index 0000000..bb655bd --- /dev/null +++ b/src/tools/featureFlags/delete.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + flagKey: z.string(), +}); + +type Params = z.infer; + +export const deleteHandler = async ( + context: Context, + params: Params +) => { + const { flagKey } = params; + const projectId = await context.getProjectId(); + + const flagResult = await context.api + .featureFlags({ projectId }) + .findByKey({ key: flagKey }); + if (!flagResult.success) { + throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); + } + + if (!flagResult.data) { + return { + content: [{ type: "text", text: "Feature flag is already deleted." }], + }; + } + + const deleteResult = await context.api.featureFlags({ projectId }).delete({ + flagId: flagResult.data.id, + }); + if (!deleteResult.success) { + throw new Error(`Failed to delete feature flag: ${deleteResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(deleteResult.data) }], + }; +}; + +const tool = (): Tool => ({ + name: "delete-feature-flag", + description: ` + - Use this tool to delete a feature flag in the project. + `, + schema, + handler: deleteHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/featureFlags/getAll.ts b/src/tools/featureFlags/getAll.ts new file mode 100644 index 0000000..a02a002 --- /dev/null +++ b/src/tools/featureFlags/getAll.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({}); + +type Params = z.infer; + +export const getAllHandler = async (context: Context, _params: Params) => { + const projectId = await context.getProjectId(); + + const flagsResult = await context.api.featureFlags({ projectId }).list(); + if (!flagsResult.success) { + throw new Error(`Failed to get feature flags: ${flagsResult.error.message}`); + } + + return { content: [{ type: "text", text: JSON.stringify(flagsResult.data) }] }; +}; + +const tool = (): Tool => ({ + name: "feature-flag-get-all", + description: ` + - Use this tool to get all feature flags in the project. + `, + schema, + handler: getAllHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/featureFlags/getDefinition.ts b/src/tools/featureFlags/getDefinition.ts new file mode 100644 index 0000000..2b6498c --- /dev/null +++ b/src/tools/featureFlags/getDefinition.ts @@ -0,0 +1,83 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + flagId: z.string().optional(), + flagKey: z.string().optional(), +}); + +type Params = z.infer; + +export const getDefinitionHandler = async ( + context: Context, + { flagId, flagKey }: Params +) => { + if (!flagId && !flagKey) { + return { + content: [ + { + type: "text", + text: "Error: Either flagId or flagKey must be provided.", + }, + ], + }; + } + + const projectId = await context.getProjectId(); + + if (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}`); + } + return { + content: [{ type: "text", text: JSON.stringify(flagResult.data) }], + }; + } + + if (flagKey) { + const flagResult = await context.api + .featureFlags({ projectId }) + .findByKey({ key: flagKey }); + if (!flagResult.success) { + throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); + } + if (flagResult.data) { + return { + content: [{ type: "text", text: JSON.stringify(flagResult.data) }], + }; + } + return { + content: [ + { + type: "text", + text: `Error: Flag with key "${flagKey}" not found.`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: "Error: Could not determine or find the feature flag.", + }, + ], + }; +}; + +const tool = (): Tool => ({ + name: "feature-flag-get-definition", + description: ` + - Use this tool to get the definition of a feature flag. + - You can provide either the flagId or the flagKey. + - If you provide both, the flagId will be used. + `, + schema, + handler: getDefinitionHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/featureFlags/update.ts b/src/tools/featureFlags/update.ts new file mode 100644 index 0000000..150da1a --- /dev/null +++ b/src/tools/featureFlags/update.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { UpdateFeatureFlagInputSchema } from "../../schema/flags"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + flagKey: z.string(), + data: UpdateFeatureFlagInputSchema, +}); + +type Params = z.infer; + +export const updateHandler = async ( + context: Context, + params: Params +) => { + const { flagKey, data } = params; + const projectId = await context.getProjectId(); + + const flagResult = await context.api.featureFlags({ projectId }).update({ + key: flagKey, + data: data, + }); + + if (!flagResult.success) { + throw new Error(`Failed to update feature flag: ${flagResult.error.message}`); + } + + const featureFlagWithUrl = { + ...flagResult.data, + url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, + }; + + return { + content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], + }; +}; + +const tool = (): Tool => ({ + name: "update-feature-flag", + description: `Update a new feature flag in the project. + - To enable a feature flag, you should make sure it is active and the rollout percentage is set to 100 for the group you want to target. + - To disable a feature flag, you should make sure it is inactive, you can keep the rollout percentage as it is. + `, + schema, + handler: updateHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..1569657 --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,92 @@ +import type { Context, Tool, ZodObjectAny } from "./types"; + +// Feature Flags +import getFeatureFlagDefinition from "./featureFlags/getDefinition"; +import getAllFeatureFlags from "./featureFlags/getAll"; +import createFeatureFlag from "./featureFlags/create"; +import updateFeatureFlag from "./featureFlags/update"; +import deleteFeatureFlag from "./featureFlags/delete"; + +// Organizations +import getOrganizations from "./organizations/getOrganizations"; +import setActiveOrganization from "./organizations/setActive"; +import getOrganizationDetails from "./organizations/getDetails"; + +// Projects +import getProjects from "./projects/getProjects"; +import setActiveProject from "./projects/setActive"; +import propertyDefinitions from "./projects/propertyDefinitions"; + +// Documentation +import searchDocs from "./documentation/searchDocs"; + +// Error Tracking +import listErrors from "./errorTracking/listErrors"; +import errorDetails from "./errorTracking/errorDetails"; + +// Insights +import getAllInsights from "./insights/getAll"; +import getInsight from "./insights/get"; +import createInsight from "./insights/create"; +import updateInsight from "./insights/update"; +import deleteInsight from "./insights/delete"; +import getSqlInsight from "./insights/getSqlInsight"; + +// Dashboards +import getAllDashboards from "./dashboards/getAll"; +import getDashboard from "./dashboards/get"; +import createDashboard from "./dashboards/create"; +import updateDashboard from "./dashboards/update"; +import deleteDashboard from "./dashboards/delete"; +import addInsightToDashboard from "./dashboards/addInsight"; + +// LLM Observability +import getLLMCosts from "./llmObservability/getLLMCosts"; + +const tools = (_context: Context): Tool[] => [ + // Feature Flags + getFeatureFlagDefinition(), + getAllFeatureFlags(), + createFeatureFlag(), + updateFeatureFlag(), + deleteFeatureFlag(), + + // Organizations + getOrganizations(), + setActiveOrganization(), + getOrganizationDetails(), + + // Projects + getProjects(), + setActiveProject(), + propertyDefinitions(), + + // Documentation + searchDocs(), + + // Error Tracking + listErrors(), + errorDetails(), + + // Insights + getAllInsights(), + getInsight(), + createInsight(), + updateInsight(), + deleteInsight(), + getSqlInsight(), + + // Dashboards + getAllDashboards(), + getDashboard(), + createDashboard(), + updateDashboard(), + deleteDashboard(), + addInsightToDashboard(), + + // LLM Observability + getLLMCosts(), +]; + +export default tools; +export type { Tool, Context, State } from "./types"; \ No newline at end of file diff --git a/src/tools/insights/create.ts b/src/tools/insights/create.ts new file mode 100644 index 0000000..20daa57 --- /dev/null +++ b/src/tools/insights/create.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { CreateInsightInputSchema } from "../../schema/insights"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + data: CreateInsightInputSchema, +}); + +type Params = z.infer; + +export const createHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).create({ data }); + if (!insightResult.success) { + throw new Error(`Failed to create insight: ${insightResult.error.message}`); + } + + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; +}; + +const tool = (): Tool => ({ + name: "insight-create-from-query", + description: ` + - You can use this to save a query as an insight. You should only do this with a valid query that you have seen, or one you have modified slightly. + - If the user wants to see data, you should use the "get-sql-insight" tool to get that data instead. + - An insight requires a name, query, and other optional properties. + - The query should use HogQL, which is a variant of Clickhouse SQL. Here is an example query: + Here is an example of a validquery: + { + "kind": "DataVisualizationNode", + "source": { + "kind": "HogQLQuery", + "query": "SELECT\\n event,\\n count() AS event_count\\nFROM\\n events\\nWHERE\\n timestamp >= now() - INTERVAL 7 day\\nGROUP BY\\n event\\nORDER BY\\n event_count DESC\\nLIMIT 10", + "explain": true, + "filters": { + "dateRange": { + "date_from": "-7d" + } + } + }, + } + `, + schema, + handler: createHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/insights/delete.ts b/src/tools/insights/delete.ts new file mode 100644 index 0000000..61261db --- /dev/null +++ b/src/tools/insights/delete.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + insightId: z.number(), +}); + +type Params = z.infer; + +export const deleteHandler = async (context: Context, params: Params) => { + const { insightId } = params; + const projectId = await context.getProjectId(); + const result = await context.api.insights({ projectId }).delete({ insightId }); + + if (!result.success) { + throw new Error(`Failed to delete insight: ${result.error.message}`); + } + + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; +}; + +const tool = (): Tool => ({ + name: "insight-delete", + description: ` + - Delete an insight by ID (soft delete - marks as deleted). + `, + schema, + handler: deleteHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/insights/get.ts b/src/tools/insights/get.ts new file mode 100644 index 0000000..058f6cb --- /dev/null +++ b/src/tools/insights/get.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + insightId: z.number(), +}); + +type Params = z.infer; + +export const getHandler = async (context: Context, params: Params) => { + const { insightId } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).get({ insightId }); + if (!insightResult.success) { + throw new Error(`Failed to get insight: ${insightResult.error.message}`); + } + + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; +}; + +const tool = (): Tool => ({ + name: "insight-get", + description: ` + - Get a specific insight by ID. + `, + schema, + handler: getHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/insights/getAll.ts b/src/tools/insights/getAll.ts new file mode 100644 index 0000000..ea9e6c9 --- /dev/null +++ b/src/tools/insights/getAll.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { ListInsightsSchema } from "../../schema/insights"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + data: ListInsightsSchema.optional(), +}); + +type Params = z.infer; + +export const getAllHandler = async (context: Context, params: Params) => { + const { data } = params; + const projectId = await context.getProjectId(); + const insightsResult = await context.api + .insights({ projectId }) + .list({ params: data }); + + if (!insightsResult.success) { + throw new Error(`Failed to get insights: ${insightsResult.error.message}`); + } + + const insightsWithUrls = insightsResult.data.map((insight) => ({ + ...insight, + url: `${getProjectBaseUrl(projectId)}/insights/${insight.short_id}`, + })); + + return { content: [{ type: "text", text: JSON.stringify(insightsWithUrls) }] }; +}; + +const tool = (): Tool => ({ + name: "insights-get-all", + description: ` + - Get all insights in the project with optional filtering. + - Can filter by saved status, favorited status, or search term. + `, + schema, + handler: getAllHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/insights/getSqlInsight.ts b/src/tools/insights/getSqlInsight.ts new file mode 100644 index 0000000..02929cf --- /dev/null +++ b/src/tools/insights/getSqlInsight.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + query: z + .string() + .max(1000) + .describe( + "Your natural language query describing the SQL insight (max 1000 characters).", + ), +}); + +type Params = z.infer; + +export const getSqlInsightHandler = async (context: Context, params: Params) => { + const { query } = params; + const projectId = await context.getProjectId(); + + const result = await context.api.insights({ projectId }).sqlInsight({ query }); + if (!result.success) { + throw new Error(`Failed to execute SQL insight: ${result.error.message}`); + } + + if (result.data.results.length === 0) { + return { + content: [ + { + type: "text", + text: "Received an empty SQL insight or no data in the stream.", + }, + ], + }; + } + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; +}; + +const tool = (): Tool => ({ + name: "get-sql-insight", + description: ` + - Queries project's PostHog data warehouse based on a provided natural language question - don't provide SQL query as input but describe the output you want. + - Data warehouse schema includes data like events and persons. + - Use this tool to get a quick answer to a question about the data in the project, which can't be answered using other, more dedicated tools. + - Fetches the result as a Server-Sent Events (SSE) stream and provides the concatenated data content. + - When giving the results back to the user, first show the SQL query that was used, then briefly explain the query, then provide results in reasily readable format. + - You should also offer to save the query as an insight if the user wants to. + `, + schema, + handler: getSqlInsightHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/insights/update.ts b/src/tools/insights/update.ts new file mode 100644 index 0000000..9ba1752 --- /dev/null +++ b/src/tools/insights/update.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; +import { UpdateInsightInputSchema } from "../../schema/insights"; +import { getProjectBaseUrl } from "../../lib/utils/api"; + +const schema = z.object({ + insightId: z.number(), + data: UpdateInsightInputSchema, +}); + +type Params = z.infer; + +export const updateHandler = async (context: Context, params: Params) => { + const { insightId, data } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).update({ + insightId, + data, + }); + + if (!insightResult.success) { + throw new Error(`Failed to update insight: ${insightResult.error.message}`); + } + + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; +}; + +const tool = (): Tool => ({ + name: "insight-update", + description: ` + - Update an existing insight by ID. + - Can update name, description, filters, and other properties. + `, + schema, + handler: updateHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/llmObservability/getLLMCosts.ts b/src/tools/llmObservability/getLLMCosts.ts new file mode 100644 index 0000000..0c45151 --- /dev/null +++ b/src/tools/llmObservability/getLLMCosts.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + projectId: z.string(), + days: z.number().optional(), +}); + +type Params = z.infer; + +export const getLLMCostsHandler = async (context: Context, params: Params) => { + const { projectId, days } = params; + + const trendsQuery = { + kind: "TrendsQuery", + dateRange: { + date_from: `-${days || 6}d`, + date_to: null, + }, + filterTestAccounts: true, + series: [ + { + event: "$ai_generation", + name: "$ai_generation", + math: "sum", + math_property: "$ai_total_cost_usd", + kind: "EventsNode", + }, + ], + breakdownFilter: { + breakdown_type: "event", + breakdown: "$ai_model", + }, + }; + + const costsResult = await context.api + .query({ projectId }) + .execute({ queryBody: trendsQuery }); + if (!costsResult.success) { + throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`); + } + return { + content: [{ type: "text", text: JSON.stringify(costsResult.data.results) }], + }; +}; + +const tool = (): Tool => ({ + name: "get-llm-total-costs-for-project", + description: ` + - Fetches the total LLM daily costs for each model for a project over a given number of days. + - If no number of days is provided, it defaults to 7. + - The results are sorted by model name. + - The total cost is rounded to 4 decimal places. + - The query is executed against the project's data warehouse. + - Show the results as a Markdown formatted table with the following information for each model: + - Model name + - Total cost in USD + - Each day's date + - Each day's cost in USD + - Write in bold the model name with the highest total cost. + - Properly render the markdown table in the response. + `, + schema, + handler: getLLMCostsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/organizations/getDetails.ts b/src/tools/organizations/getDetails.ts new file mode 100644 index 0000000..0387076 --- /dev/null +++ b/src/tools/organizations/getDetails.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({}); + +type Params = z.infer; + +export const getDetailsHandler = async (context: Context, _params: Params) => { + const orgId = await context.getOrgID(); + + const orgResult = await context.api.organizations().get({ orgId }); + + if (!orgResult.success) { + throw new Error( + `Failed to get organization details: ${orgResult.error.message}`, + ); + } + + return { + content: [{ type: "text", text: JSON.stringify(orgResult.data) }], + }; +}; + +const tool = (): Tool => ({ + name: "organization-details-get", + description: ` + - Use this tool to get the details of the active organization. + `, + schema, + handler: getDetailsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/organizations/getOrganizations.ts b/src/tools/organizations/getOrganizations.ts new file mode 100644 index 0000000..c60cabf --- /dev/null +++ b/src/tools/organizations/getOrganizations.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({}); + +type Params = z.infer; + +export const getOrganizationsHandler = async (context: Context, _params: Params) => { + const orgsResult = await context.api.organizations().list(); + if (!orgsResult.success) { + throw new Error(`Failed to get organizations: ${orgsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(orgsResult.data) }], + }; +}; + +const tool = (): Tool => ({ + name: "organizations-get", + description: ` + - Use this tool to get the organizations the user has access to. + `, + schema, + handler: getOrganizationsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/organizations/setActive.ts b/src/tools/organizations/setActive.ts new file mode 100644 index 0000000..c0c7a04 --- /dev/null +++ b/src/tools/organizations/setActive.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + orgId: z.string(), +}); + +type Params = z.infer; + +export const setActiveHandler = async (context: Context, params: Params) => { + const { orgId } = params; + await context.cache.set("orgId", orgId); + + return { + content: [{ type: "text", text: `Switched to organization ${orgId}` }], + }; +}; + +const tool = (): Tool => ({ + name: "organization-set-active", + description: ` + - Use this tool to set the active organization. + `, + schema, + handler: setActiveHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/projects/getProjects.ts b/src/tools/projects/getProjects.ts new file mode 100644 index 0000000..225f146 --- /dev/null +++ b/src/tools/projects/getProjects.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({}); + +type Params = z.infer; + +export const getProjectsHandler = async (context: Context, _params: Params) => { + const orgId = await context.getOrgID(); + const projectsResult = await context.api.organizations().projects({ orgId }).list(); + + if (!projectsResult.success) { + throw new Error(`Failed to get projects: ${projectsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(projectsResult.data) }], + }; +}; + +const tool = (): Tool => ({ + name: "projects-get", + description: ` + - Fetches projects that the user has access to - the orgId is optional. + - Use this tool before you use any other tools (besides organization-* and docs-search) to allow user to select the project they want to use for subsequent requests. + `, + schema, + handler: getProjectsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/projects/propertyDefinitions.ts b/src/tools/projects/propertyDefinitions.ts new file mode 100644 index 0000000..f0155bc --- /dev/null +++ b/src/tools/projects/propertyDefinitions.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({}); + +type Params = z.infer; + +export const propertyDefinitionsHandler = async (context: Context, _params: Params) => { + const projectId = await context.getProjectId(); + + const propDefsResult = await context.api.projects().propertyDefinitions({ projectId }); + + if (!propDefsResult.success) { + throw new Error( + `Failed to get property definitions: ${propDefsResult.error.message}`, + ); + } + return { + content: [{ type: "text", text: JSON.stringify(propDefsResult.data) }], + }; +}; + +const tool = (): Tool => ({ + name: "property-definitions", + description: ` + - Use this tool to get the property definitions of the active project. + `, + schema, + handler: propertyDefinitionsHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/projects/setActive.ts b/src/tools/projects/setActive.ts new file mode 100644 index 0000000..2b73fd0 --- /dev/null +++ b/src/tools/projects/setActive.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import type { Context, Tool } from "../types"; + +const schema = z.object({ + projectId: z.string(), +}); + +type Params = z.infer; + +export const setActiveHandler = async (context: Context, params: Params) => { + const { projectId } = params; + + await context.cache.set("projectId", projectId); + + return { + content: [{ type: "text", text: `Switched to project ${projectId}` }], + }; +}; + +const tool = (): Tool => ({ + name: "project-set-active", + description: ` + - Use this tool to set the active project. + `, + schema, + handler: setActiveHandler, +}); + +export default tool; \ No newline at end of file diff --git a/src/tools/types.ts b/src/tools/types.ts new file mode 100644 index 0000000..eefe8f1 --- /dev/null +++ b/src/tools/types.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; +import { ApiClient } from "../api/client"; +import { DurableObjectCache } from "../lib/utils/cache/DurableObjectCache"; + +export type State = { + projectId: string | undefined; + orgId: string | undefined; + distinctId: string | undefined; +}; + +export type Context = { + api: ApiClient; + cache: DurableObjectCache; + env: Env; + getProjectId: () => Promise; + getOrgID: () => Promise; + getDistinctId: () => Promise; +}; + +export type Tool = { + name: string; + description: string; + schema: TSchema; + handler: (context: Context, params: z.infer) => Promise; +}; + +export type ZodObjectAny = z.ZodObject; \ No newline at end of file diff --git a/tests/api/client.test.ts b/tests/api/client.test.ts index 3324efa..0d0ca81 100644 --- a/tests/api/client.test.ts +++ b/tests/api/client.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; -import { type api, ApiClient } from "../../src/api/client"; +import { ApiClient } from "../../src/api/client"; const API_BASE_URL = process.env.TEST_API_BASE_URL || "http://localhost:8010"; const API_TOKEN = process.env.TEST_API_TOKEN; describe("API Client Integration Tests", () => { - let client: ReturnType; + let client: ApiClient; let testOrgId: string; let testProjectId: string; From 1c829c8be03463e4536fb19d581eeb612b1300fc Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Tue, 22 Jul 2025 14:39:33 +0200 Subject: [PATCH 2/4] formatting --- src/index.ts | 10 +- src/tools/dashboards/addInsight.ts | 49 ++++---- src/tools/dashboards/create.ts | 34 +++--- src/tools/dashboards/delete.ts | 26 ++--- src/tools/dashboards/get.ts | 28 +++-- src/tools/dashboards/getAll.ts | 28 +++-- src/tools/dashboards/update.ts | 46 ++++---- src/tools/documentation/searchDocs.ts | 40 +++---- src/tools/errorTracking/errorDetails.ts | 60 +++++----- src/tools/errorTracking/listErrors.ts | 66 +++++------ src/tools/featureFlags/create.ts | 65 +++++------ src/tools/featureFlags/delete.ts | 67 +++++------ src/tools/featureFlags/getAll.ts | 22 ++-- src/tools/featureFlags/getDefinition.ts | 123 ++++++++++---------- src/tools/featureFlags/update.ts | 59 +++++----- src/tools/index.ts | 86 +++++++------- src/tools/insights/create.ts | 34 +++--- src/tools/insights/delete.ts | 26 ++--- src/tools/insights/get.ts | 34 +++--- src/tools/insights/getAll.ts | 42 ++++--- src/tools/insights/getSqlInsight.ts | 54 +++++---- src/tools/insights/update.ts | 48 ++++---- src/tools/llmObservability/getLLMCosts.ts | 76 ++++++------ src/tools/organizations/getDetails.ts | 34 +++--- src/tools/organizations/getOrganizations.ts | 26 ++--- src/tools/organizations/setActive.ts | 22 ++-- src/tools/projects/getProjects.ts | 28 ++--- src/tools/projects/propertyDefinitions.ts | 28 +++-- src/tools/projects/setActive.ts | 22 ++-- src/tools/types.ts | 34 +++--- 30 files changed, 640 insertions(+), 677 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8139c74..5714803 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { McpServer, type ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpAgent } from "agents/mcp"; -import { z } from "zod"; +import type { z } from "zod"; import { ApiClient } from "./api/client"; import { getPostHogClient } from "./lib/client"; @@ -171,14 +171,10 @@ export class MyMCP extends McpAgent { const allTools = tools(context); for (const tool of allTools) { - this.registerTool( - tool.name, - tool.description, - tool.schema.shape, - async (params) => tool.handler(context, params), + this.registerTool(tool.name, tool.description, tool.schema.shape, async (params) => + tool.handler(context, params), ); } - } } diff --git a/src/tools/dashboards/addInsight.ts b/src/tools/dashboards/addInsight.ts index 447164c..ac1f7f6 100644 --- a/src/tools/dashboards/addInsight.ts +++ b/src/tools/dashboards/addInsight.ts @@ -4,48 +4,47 @@ import { AddInsightToDashboardSchema } from "../../schema/dashboards"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - data: AddInsightToDashboardSchema, + data: AddInsightToDashboardSchema, }); type Params = z.infer; export const addInsightHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); + const { data } = params; + const projectId = await context.getProjectId(); - const insightResult = await context.api - .insights({ projectId }) - .get({ insightId: data.insight_id }); - - if (!insightResult.success) { - throw new Error(`Failed to get insight: ${insightResult.error.message}`); - } + const insightResult = await context.api + .insights({ projectId }) + .get({ insightId: data.insight_id }); - const result = await context.api.dashboards({ projectId }).addInsight({ data }); + if (!insightResult.success) { + throw new Error(`Failed to get insight: ${insightResult.error.message}`); + } - if (!result.success) { - throw new Error(`Failed to add insight to dashboard: ${result.error.message}`); - } + const result = await context.api.dashboards({ projectId }).addInsight({ data }); + if (!result.success) { + throw new Error(`Failed to add insight to dashboard: ${result.error.message}`); + } - const resultWithUrls = { - ...result.data, - dashboard_url: `${getProjectBaseUrl(projectId)}/dashboard/${data.dashboard_id}`, - insight_url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; + const resultWithUrls = { + ...result.data, + dashboard_url: `${getProjectBaseUrl(projectId)}/dashboard/${data.dashboard_id}`, + insight_url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; - return { content: [{ type: "text", text: JSON.stringify(resultWithUrls) }] }; + return { content: [{ type: "text", text: JSON.stringify(resultWithUrls) }] }; }; const tool = (): Tool => ({ - name: "add-insight-to-dashboard", - description: ` + name: "add-insight-to-dashboard", + description: ` - Add an existing insight to a dashboard. - Requires insight ID and dashboard ID. - Optionally supports layout and color customization. `, - schema, - handler: addInsightHandler, + schema, + handler: addInsightHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/dashboards/create.ts b/src/tools/dashboards/create.ts index 4af35c1..a4b77b2 100644 --- a/src/tools/dashboards/create.ts +++ b/src/tools/dashboards/create.ts @@ -4,36 +4,36 @@ import { CreateDashboardInputSchema } from "../../schema/dashboards"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - data: CreateDashboardInputSchema, + data: CreateDashboardInputSchema, }); type Params = z.infer; export const createHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - const dashboardResult = await context.api.dashboards({ projectId }).create({ data }); + const { data } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api.dashboards({ projectId }).create({ data }); - if (!dashboardResult.success) { - throw new Error(`Failed to create dashboard: ${dashboardResult.error.message}`); - } + if (!dashboardResult.success) { + throw new Error(`Failed to create dashboard: ${dashboardResult.error.message}`); + } - const dashboardWithUrl = { - ...dashboardResult.data, - url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, - }; + const dashboardWithUrl = { + ...dashboardResult.data, + url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, + }; - return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; + return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; }; const tool = (): Tool => ({ - name: "dashboard-create", - description: ` + name: "dashboard-create", + description: ` - Create a new dashboard in the project. - Requires name and optional description, tags, and other properties. `, - schema, - handler: createHandler, + schema, + handler: createHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/dashboards/delete.ts b/src/tools/dashboards/delete.ts index 5f85419..9fec2f7 100644 --- a/src/tools/dashboards/delete.ts +++ b/src/tools/dashboards/delete.ts @@ -2,30 +2,30 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - dashboardId: z.number(), + dashboardId: z.number(), }); type Params = z.infer; export const deleteHandler = async (context: Context, params: Params) => { - const { dashboardId } = params; - const projectId = await context.getProjectId(); - const result = await context.api.dashboards({ projectId }).delete({ dashboardId }); + const { dashboardId } = params; + const projectId = await context.getProjectId(); + const result = await context.api.dashboards({ projectId }).delete({ dashboardId }); - if (!result.success) { - throw new Error(`Failed to delete dashboard: ${result.error.message}`); - } + if (!result.success) { + throw new Error(`Failed to delete dashboard: ${result.error.message}`); + } - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; }; const tool = (): Tool => ({ - name: "dashboard-delete", - description: ` + name: "dashboard-delete", + description: ` - Delete a dashboard by ID (soft delete - marks as deleted). `, - schema, - handler: deleteHandler, + schema, + handler: deleteHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/dashboards/get.ts b/src/tools/dashboards/get.ts index 1f5cab3..f230d07 100644 --- a/src/tools/dashboards/get.ts +++ b/src/tools/dashboards/get.ts @@ -2,32 +2,30 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - dashboardId: z.number(), + dashboardId: z.number(), }); type Params = z.infer; export const getHandler = async (context: Context, params: Params) => { - const { dashboardId } = params; - const projectId = await context.getProjectId(); - const dashboardResult = await context.api - .dashboards({ projectId }) - .get({ dashboardId }); + const { dashboardId } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api.dashboards({ projectId }).get({ dashboardId }); - if (!dashboardResult.success) { - throw new Error(`Failed to get dashboard: ${dashboardResult.error.message}`); - } + if (!dashboardResult.success) { + throw new Error(`Failed to get dashboard: ${dashboardResult.error.message}`); + } - return { content: [{ type: "text", text: JSON.stringify(dashboardResult.data) }] }; + return { content: [{ type: "text", text: JSON.stringify(dashboardResult.data) }] }; }; const tool = (): Tool => ({ - name: "dashboard-get", - description: ` + name: "dashboard-get", + description: ` - Get a specific dashboard by ID. `, - schema, - handler: getHandler, + schema, + handler: getHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/dashboards/getAll.ts b/src/tools/dashboards/getAll.ts index 55d0e97..2b58641 100644 --- a/src/tools/dashboards/getAll.ts +++ b/src/tools/dashboards/getAll.ts @@ -3,33 +3,31 @@ import type { Context, Tool } from "../types"; import { ListDashboardsSchema } from "../../schema/dashboards"; const schema = z.object({ - data: ListDashboardsSchema.optional(), + data: ListDashboardsSchema.optional(), }); type Params = z.infer; export const getAllHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - const dashboardsResult = await context.api - .dashboards({ projectId }) - .list({ params: data }); + const { data } = params; + const projectId = await context.getProjectId(); + const dashboardsResult = await context.api.dashboards({ projectId }).list({ params: data }); - if (!dashboardsResult.success) { - throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`); - } + if (!dashboardsResult.success) { + throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`); + } - return { content: [{ type: "text", text: JSON.stringify(dashboardsResult.data) }] }; + return { content: [{ type: "text", text: JSON.stringify(dashboardsResult.data) }] }; }; const tool = (): Tool => ({ - name: "dashboards-get-all", - description: ` + name: "dashboards-get-all", + description: ` - Get all dashboards in the project with optional filtering. - Can filter by pinned status, search term, or pagination. `, - schema, - handler: getAllHandler, + schema, + handler: getAllHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/dashboards/update.ts b/src/tools/dashboards/update.ts index a4e4383..e72ac96 100644 --- a/src/tools/dashboards/update.ts +++ b/src/tools/dashboards/update.ts @@ -4,39 +4,39 @@ import { UpdateDashboardInputSchema } from "../../schema/dashboards"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - dashboardId: z.number(), - data: UpdateDashboardInputSchema, + dashboardId: z.number(), + data: UpdateDashboardInputSchema, }); type Params = z.infer; export const updateHandler = async (context: Context, params: Params) => { - const { dashboardId, data } = params; - const projectId = await context.getProjectId(); - const dashboardResult = await context.api - .dashboards({ projectId }) - .update({ dashboardId, data }); - - if (!dashboardResult.success) { - throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`); - } - - const dashboardWithUrl = { - ...dashboardResult.data, - url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; + const { dashboardId, data } = params; + const projectId = await context.getProjectId(); + const dashboardResult = await context.api + .dashboards({ projectId }) + .update({ dashboardId, data }); + + if (!dashboardResult.success) { + throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`); + } + + const dashboardWithUrl = { + ...dashboardResult.data, + url: `${getProjectBaseUrl(projectId)}/dashboard/${dashboardResult.data.id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(dashboardWithUrl) }] }; }; const tool = (): Tool => ({ - name: "dashboard-update", - description: ` + name: "dashboard-update", + description: ` - Update an existing dashboard by ID. - Can update name, description, pinned status or tags. `, - schema, - handler: updateHandler, + schema, + handler: updateHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/documentation/searchDocs.ts b/src/tools/documentation/searchDocs.ts index 68170e5..32f3f80 100644 --- a/src/tools/documentation/searchDocs.ts +++ b/src/tools/documentation/searchDocs.ts @@ -3,37 +3,37 @@ import type { Context, Tool } from "../types"; import { docsSearch } from "../../inkeepApi"; const schema = z.object({ - query: z.string(), + query: z.string(), }); type Params = z.infer; export const searchDocsHandler = async (context: Context, params: Params) => { - const { query } = params; - const inkeepApiKey = context.env.INKEEP_API_KEY; + const { query } = params; + const inkeepApiKey = context.env.INKEEP_API_KEY; - if (!inkeepApiKey) { - return { - content: [ - { - type: "text", - text: "Error: INKEEP_API_KEY is not configured.", - }, - ], - }; - } - const resultText = await docsSearch(inkeepApiKey, query); - return { content: [{ type: "text", text: resultText }] }; + if (!inkeepApiKey) { + return { + content: [ + { + type: "text", + text: "Error: INKEEP_API_KEY is not configured.", + }, + ], + }; + } + const resultText = await docsSearch(inkeepApiKey, query); + return { content: [{ type: "text", text: resultText }] }; }; const tool = (): Tool => ({ - name: "docs-search", - description: ` + name: "docs-search", + description: ` - Use this tool to search the PostHog documentation for information that can help the user with their request. - Use it as a fallback when you cannot answer the user's request using other tools in this MCP. `, - schema, - handler: searchDocsHandler, + schema, + handler: searchDocsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/errorTracking/errorDetails.ts b/src/tools/errorTracking/errorDetails.ts index 9121a3e..be6757e 100644 --- a/src/tools/errorTracking/errorDetails.ts +++ b/src/tools/errorTracking/errorDetails.ts @@ -3,46 +3,44 @@ import type { Context, Tool } from "../types"; import { ErrorDetailsSchema } from "../../schema/errors"; const schema = z.object({ - data: ErrorDetailsSchema, + data: ErrorDetailsSchema, }); type Params = z.infer; export const errorDetailsHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - - const errorQuery = { - kind: "ErrorTrackingQuery", - dateRange: { - date_from: - data.dateFrom?.toISOString() || - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), - date_to: data.dateTo?.toISOString() || new Date().toISOString(), - }, - volumeResolution: 0, - issueId: data.issueId, - }; - - const errorsResult = await context.api - .query({ projectId }) - .execute({ queryBody: errorQuery }); - if (!errorsResult.success) { - throw new Error(`Failed to get error details: ${errorsResult.error.message}`); - } - - return { - content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], - }; + const { data } = params; + const projectId = await context.getProjectId(); + + const errorQuery = { + kind: "ErrorTrackingQuery", + dateRange: { + date_from: + data.dateFrom?.toISOString() || + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + date_to: data.dateTo?.toISOString() || new Date().toISOString(), + }, + volumeResolution: 0, + issueId: data.issueId, + }; + + const errorsResult = await context.api.query({ projectId }).execute({ queryBody: errorQuery }); + if (!errorsResult.success) { + throw new Error(`Failed to get error details: ${errorsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], + }; }; const tool = (): Tool => ({ - name: "error-details", - description: ` + name: "error-details", + description: ` - Use this tool to get the details of an error in the project. `, - schema, - handler: errorDetailsHandler, + schema, + handler: errorDetailsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/errorTracking/listErrors.ts b/src/tools/errorTracking/listErrors.ts index 44635e6..7f03713 100644 --- a/src/tools/errorTracking/listErrors.ts +++ b/src/tools/errorTracking/listErrors.ts @@ -3,49 +3,47 @@ import type { Context, Tool } from "../types"; import { ListErrorsSchema } from "../../schema/errors"; const schema = z.object({ - data: ListErrorsSchema, + data: ListErrorsSchema, }); type Params = z.infer; export const listErrorsHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - - const errorQuery = { - kind: "ErrorTrackingQuery", - orderBy: data.orderBy || "occurrences", - dateRange: { - date_from: - data.dateFrom?.toISOString() || - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), - date_to: data.dateTo?.toISOString() || new Date().toISOString(), - }, - volumeResolution: 1, - orderDirection: data.orderDirection || "DESC", - filterTestAccounts: data.filterTestAccounts ?? true, - status: data.status || "active", - }; - - const errorsResult = await context.api - .query({ projectId }) - .execute({ queryBody: errorQuery }); - if (!errorsResult.success) { - throw new Error(`Failed to list errors: ${errorsResult.error.message}`); - } - - return { - content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], - }; + const { data } = params; + const projectId = await context.getProjectId(); + + const errorQuery = { + kind: "ErrorTrackingQuery", + orderBy: data.orderBy || "occurrences", + dateRange: { + date_from: + data.dateFrom?.toISOString() || + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + date_to: data.dateTo?.toISOString() || new Date().toISOString(), + }, + volumeResolution: 1, + orderDirection: data.orderDirection || "DESC", + filterTestAccounts: data.filterTestAccounts ?? true, + status: data.status || "active", + }; + + const errorsResult = await context.api.query({ projectId }).execute({ queryBody: errorQuery }); + if (!errorsResult.success) { + throw new Error(`Failed to list errors: ${errorsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(errorsResult.data.results) }], + }; }; const tool = (): Tool => ({ - name: "list-errors", - description: ` + name: "list-errors", + description: ` - Use this tool to list errors in the project. `, - schema, - handler: listErrorsHandler, + schema, + handler: listErrorsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/featureFlags/create.ts b/src/tools/featureFlags/create.ts index 6d83ef4..657f507 100644 --- a/src/tools/featureFlags/create.ts +++ b/src/tools/featureFlags/create.ts @@ -4,50 +4,47 @@ import { FilterGroupsSchema } from "../../schema/flags"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = 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(), }); type Params = z.infer; -export const createHandler = async ( - context: Context, - params: Params -) => { - const { name, key, description, filters, active, tags } = params; - const projectId = await context.getProjectId(); - - 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}`); - } - - const featureFlagWithUrl = { - ...flagResult.data, - url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, - }; - - return { - content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], - }; +export const createHandler = async (context: Context, params: Params) => { + const { name, key, description, filters, active, tags } = params; + const projectId = await context.getProjectId(); + + 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}`); + } + + const featureFlagWithUrl = { + ...flagResult.data, + url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, + }; + + return { + content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], + }; }; const tool = (): Tool => ({ - name: "create-feature-flag", - description: `Creates a new feature flag in the project. Once you have created a feature flag, you should: + name: "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. `, - schema, - handler: createHandler, + schema, + handler: createHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/featureFlags/delete.ts b/src/tools/featureFlags/delete.ts index bb655bd..b6c0b62 100644 --- a/src/tools/featureFlags/delete.ts +++ b/src/tools/featureFlags/delete.ts @@ -2,50 +2,45 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - flagKey: z.string(), + flagKey: z.string(), }); type Params = z.infer; -export const deleteHandler = async ( - context: Context, - params: Params -) => { - const { flagKey } = params; - const projectId = await context.getProjectId(); - - const flagResult = await context.api - .featureFlags({ projectId }) - .findByKey({ key: flagKey }); - if (!flagResult.success) { - throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); - } - - if (!flagResult.data) { - return { - content: [{ type: "text", text: "Feature flag is already deleted." }], - }; - } - - const deleteResult = await context.api.featureFlags({ projectId }).delete({ - flagId: flagResult.data.id, - }); - if (!deleteResult.success) { - throw new Error(`Failed to delete feature flag: ${deleteResult.error.message}`); - } - - return { - content: [{ type: "text", text: JSON.stringify(deleteResult.data) }], - }; +export const deleteHandler = async (context: Context, params: Params) => { + const { flagKey } = params; + const projectId = await context.getProjectId(); + + const flagResult = await context.api.featureFlags({ projectId }).findByKey({ key: flagKey }); + if (!flagResult.success) { + throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); + } + + if (!flagResult.data) { + return { + content: [{ type: "text", text: "Feature flag is already deleted." }], + }; + } + + const deleteResult = await context.api.featureFlags({ projectId }).delete({ + flagId: flagResult.data.id, + }); + if (!deleteResult.success) { + throw new Error(`Failed to delete feature flag: ${deleteResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(deleteResult.data) }], + }; }; const tool = (): Tool => ({ - name: "delete-feature-flag", - description: ` + name: "delete-feature-flag", + description: ` - Use this tool to delete a feature flag in the project. `, - schema, - handler: deleteHandler, + schema, + handler: deleteHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/featureFlags/getAll.ts b/src/tools/featureFlags/getAll.ts index a02a002..78e259c 100644 --- a/src/tools/featureFlags/getAll.ts +++ b/src/tools/featureFlags/getAll.ts @@ -6,23 +6,23 @@ const schema = z.object({}); type Params = z.infer; export const getAllHandler = async (context: Context, _params: Params) => { - const projectId = await context.getProjectId(); + const projectId = await context.getProjectId(); - const flagsResult = await context.api.featureFlags({ projectId }).list(); - if (!flagsResult.success) { - throw new Error(`Failed to get feature flags: ${flagsResult.error.message}`); - } + const flagsResult = await context.api.featureFlags({ projectId }).list(); + if (!flagsResult.success) { + throw new Error(`Failed to get feature flags: ${flagsResult.error.message}`); + } - return { content: [{ type: "text", text: JSON.stringify(flagsResult.data) }] }; + return { content: [{ type: "text", text: JSON.stringify(flagsResult.data) }] }; }; const tool = (): Tool => ({ - name: "feature-flag-get-all", - description: ` + name: "feature-flag-get-all", + description: ` - Use this tool to get all feature flags in the project. `, - schema, - handler: getAllHandler, + schema, + handler: getAllHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/featureFlags/getDefinition.ts b/src/tools/featureFlags/getDefinition.ts index 2b6498c..f561cec 100644 --- a/src/tools/featureFlags/getDefinition.ts +++ b/src/tools/featureFlags/getDefinition.ts @@ -2,82 +2,79 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - flagId: z.string().optional(), - flagKey: z.string().optional(), + flagId: z.string().optional(), + flagKey: z.string().optional(), }); type Params = z.infer; -export const getDefinitionHandler = async ( - context: Context, - { flagId, flagKey }: Params -) => { - if (!flagId && !flagKey) { - return { - content: [ - { - type: "text", - text: "Error: Either flagId or flagKey must be provided.", - }, - ], - }; - } +export const getDefinitionHandler = async (context: Context, { flagId, flagKey }: Params) => { + if (!flagId && !flagKey) { + return { + content: [ + { + type: "text", + text: "Error: Either flagId or flagKey must be provided.", + }, + ], + }; + } - const projectId = await context.getProjectId(); - - if (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}`); - } - return { - content: [{ type: "text", text: JSON.stringify(flagResult.data) }], - }; - } + const projectId = await context.getProjectId(); - if (flagKey) { - const flagResult = await context.api - .featureFlags({ projectId }) - .findByKey({ key: flagKey }); - if (!flagResult.success) { - throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); - } - if (flagResult.data) { - return { - content: [{ type: "text", text: JSON.stringify(flagResult.data) }], - }; - } - return { - content: [ - { - type: "text", - text: `Error: Flag with key "${flagKey}" not found.`, - }, - ], - }; - } + if (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}`); + } + return { + content: [{ type: "text", text: JSON.stringify(flagResult.data) }], + }; + } - return { - content: [ - { - type: "text", - text: "Error: Could not determine or find the feature flag.", - }, - ], - }; + if (flagKey) { + const flagResult = await context.api + .featureFlags({ projectId }) + .findByKey({ key: flagKey }); + if (!flagResult.success) { + throw new Error(`Failed to find feature flag: ${flagResult.error.message}`); + } + if (flagResult.data) { + return { + content: [{ type: "text", text: JSON.stringify(flagResult.data) }], + }; + } + return { + content: [ + { + type: "text", + text: `Error: Flag with key "${flagKey}" not found.`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: "Error: Could not determine or find the feature flag.", + }, + ], + }; }; const tool = (): Tool => ({ - name: "feature-flag-get-definition", - description: ` + name: "feature-flag-get-definition", + description: ` - Use this tool to get the definition of a feature flag. - You can provide either the flagId or the flagKey. - If you provide both, the flagId will be used. `, - schema, - handler: getDefinitionHandler, + schema, + handler: getDefinitionHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/featureFlags/update.ts b/src/tools/featureFlags/update.ts index 150da1a..3dbb5a8 100644 --- a/src/tools/featureFlags/update.ts +++ b/src/tools/featureFlags/update.ts @@ -4,46 +4,43 @@ import { UpdateFeatureFlagInputSchema } from "../../schema/flags"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - flagKey: z.string(), - data: UpdateFeatureFlagInputSchema, + flagKey: z.string(), + data: UpdateFeatureFlagInputSchema, }); type Params = z.infer; -export const updateHandler = async ( - context: Context, - params: Params -) => { - const { flagKey, data } = params; - const projectId = await context.getProjectId(); - - const flagResult = await context.api.featureFlags({ projectId }).update({ - key: flagKey, - data: data, - }); - - if (!flagResult.success) { - throw new Error(`Failed to update feature flag: ${flagResult.error.message}`); - } - - const featureFlagWithUrl = { - ...flagResult.data, - url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, - }; - - return { - content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], - }; +export const updateHandler = async (context: Context, params: Params) => { + const { flagKey, data } = params; + const projectId = await context.getProjectId(); + + const flagResult = await context.api.featureFlags({ projectId }).update({ + key: flagKey, + data: data, + }); + + if (!flagResult.success) { + throw new Error(`Failed to update feature flag: ${flagResult.error.message}`); + } + + const featureFlagWithUrl = { + ...flagResult.data, + url: `${getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`, + }; + + return { + content: [{ type: "text", text: JSON.stringify(featureFlagWithUrl) }], + }; }; const tool = (): Tool => ({ - name: "update-feature-flag", - description: `Update a new feature flag in the project. + name: "update-feature-flag", + description: `Update a new feature flag in the project. - To enable a feature flag, you should make sure it is active and the rollout percentage is set to 100 for the group you want to target. - To disable a feature flag, you should make sure it is inactive, you can keep the rollout percentage as it is. `, - schema, - handler: updateHandler, + schema, + handler: updateHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/index.ts b/src/tools/index.ts index 1569657..90f3cef 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -44,49 +44,49 @@ import addInsightToDashboard from "./dashboards/addInsight"; import getLLMCosts from "./llmObservability/getLLMCosts"; const tools = (_context: Context): Tool[] => [ - // Feature Flags - getFeatureFlagDefinition(), - getAllFeatureFlags(), - createFeatureFlag(), - updateFeatureFlag(), - deleteFeatureFlag(), - - // Organizations - getOrganizations(), - setActiveOrganization(), - getOrganizationDetails(), - - // Projects - getProjects(), - setActiveProject(), - propertyDefinitions(), - - // Documentation - searchDocs(), - - // Error Tracking - listErrors(), - errorDetails(), - - // Insights - getAllInsights(), - getInsight(), - createInsight(), - updateInsight(), - deleteInsight(), - getSqlInsight(), - - // Dashboards - getAllDashboards(), - getDashboard(), - createDashboard(), - updateDashboard(), - deleteDashboard(), - addInsightToDashboard(), - - // LLM Observability - getLLMCosts(), + // Feature Flags + getFeatureFlagDefinition(), + getAllFeatureFlags(), + createFeatureFlag(), + updateFeatureFlag(), + deleteFeatureFlag(), + + // Organizations + getOrganizations(), + setActiveOrganization(), + getOrganizationDetails(), + + // Projects + getProjects(), + setActiveProject(), + propertyDefinitions(), + + // Documentation + searchDocs(), + + // Error Tracking + listErrors(), + errorDetails(), + + // Insights + getAllInsights(), + getInsight(), + createInsight(), + updateInsight(), + deleteInsight(), + getSqlInsight(), + + // Dashboards + getAllDashboards(), + getDashboard(), + createDashboard(), + updateDashboard(), + deleteDashboard(), + addInsightToDashboard(), + + // LLM Observability + getLLMCosts(), ]; export default tools; -export type { Tool, Context, State } from "./types"; \ No newline at end of file +export type { Tool, Context, State } from "./types"; diff --git a/src/tools/insights/create.ts b/src/tools/insights/create.ts index 20daa57..a7082f0 100644 --- a/src/tools/insights/create.ts +++ b/src/tools/insights/create.ts @@ -4,30 +4,30 @@ import { CreateInsightInputSchema } from "../../schema/insights"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - data: CreateInsightInputSchema, + data: CreateInsightInputSchema, }); type Params = z.infer; export const createHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - const insightResult = await context.api.insights({ projectId }).create({ data }); - if (!insightResult.success) { - throw new Error(`Failed to create insight: ${insightResult.error.message}`); - } + const { data } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).create({ data }); + if (!insightResult.success) { + throw new Error(`Failed to create insight: ${insightResult.error.message}`); + } - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; }; const tool = (): Tool => ({ - name: "insight-create-from-query", - description: ` + name: "insight-create-from-query", + description: ` - You can use this to save a query as an insight. You should only do this with a valid query that you have seen, or one you have modified slightly. - If the user wants to see data, you should use the "get-sql-insight" tool to get that data instead. - An insight requires a name, query, and other optional properties. @@ -47,8 +47,8 @@ const tool = (): Tool => ({ }, } `, - schema, - handler: createHandler, + schema, + handler: createHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/insights/delete.ts b/src/tools/insights/delete.ts index 61261db..72493fb 100644 --- a/src/tools/insights/delete.ts +++ b/src/tools/insights/delete.ts @@ -2,30 +2,30 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - insightId: z.number(), + insightId: z.number(), }); type Params = z.infer; export const deleteHandler = async (context: Context, params: Params) => { - const { insightId } = params; - const projectId = await context.getProjectId(); - const result = await context.api.insights({ projectId }).delete({ insightId }); + const { insightId } = params; + const projectId = await context.getProjectId(); + const result = await context.api.insights({ projectId }).delete({ insightId }); - if (!result.success) { - throw new Error(`Failed to delete insight: ${result.error.message}`); - } + if (!result.success) { + throw new Error(`Failed to delete insight: ${result.error.message}`); + } - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; }; const tool = (): Tool => ({ - name: "insight-delete", - description: ` + name: "insight-delete", + description: ` - Delete an insight by ID (soft delete - marks as deleted). `, - schema, - handler: deleteHandler, + schema, + handler: deleteHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/insights/get.ts b/src/tools/insights/get.ts index 058f6cb..e274fc8 100644 --- a/src/tools/insights/get.ts +++ b/src/tools/insights/get.ts @@ -3,34 +3,34 @@ import type { Context, Tool } from "../types"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - insightId: z.number(), + insightId: z.number(), }); type Params = z.infer; export const getHandler = async (context: Context, params: Params) => { - const { insightId } = params; - const projectId = await context.getProjectId(); - const insightResult = await context.api.insights({ projectId }).get({ insightId }); - if (!insightResult.success) { - throw new Error(`Failed to get insight: ${insightResult.error.message}`); - } + const { insightId } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).get({ insightId }); + if (!insightResult.success) { + throw new Error(`Failed to get insight: ${insightResult.error.message}`); + } - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; }; const tool = (): Tool => ({ - name: "insight-get", - description: ` + name: "insight-get", + description: ` - Get a specific insight by ID. `, - schema, - handler: getHandler, + schema, + handler: getHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/insights/getAll.ts b/src/tools/insights/getAll.ts index ea9e6c9..ece238e 100644 --- a/src/tools/insights/getAll.ts +++ b/src/tools/insights/getAll.ts @@ -4,38 +4,36 @@ import { ListInsightsSchema } from "../../schema/insights"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - data: ListInsightsSchema.optional(), + data: ListInsightsSchema.optional(), }); type Params = z.infer; export const getAllHandler = async (context: Context, params: Params) => { - const { data } = params; - const projectId = await context.getProjectId(); - const insightsResult = await context.api - .insights({ projectId }) - .list({ params: data }); - - if (!insightsResult.success) { - throw new Error(`Failed to get insights: ${insightsResult.error.message}`); - } - - const insightsWithUrls = insightsResult.data.map((insight) => ({ - ...insight, - url: `${getProjectBaseUrl(projectId)}/insights/${insight.short_id}`, - })); - - return { content: [{ type: "text", text: JSON.stringify(insightsWithUrls) }] }; + const { data } = params; + const projectId = await context.getProjectId(); + const insightsResult = await context.api.insights({ projectId }).list({ params: data }); + + if (!insightsResult.success) { + throw new Error(`Failed to get insights: ${insightsResult.error.message}`); + } + + const insightsWithUrls = insightsResult.data.map((insight) => ({ + ...insight, + url: `${getProjectBaseUrl(projectId)}/insights/${insight.short_id}`, + })); + + return { content: [{ type: "text", text: JSON.stringify(insightsWithUrls) }] }; }; const tool = (): Tool => ({ - name: "insights-get-all", - description: ` + name: "insights-get-all", + description: ` - Get all insights in the project with optional filtering. - Can filter by saved status, favorited status, or search term. `, - schema, - handler: getAllHandler, + schema, + handler: getAllHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/insights/getSqlInsight.ts b/src/tools/insights/getSqlInsight.ts index 02929cf..9e3a049 100644 --- a/src/tools/insights/getSqlInsight.ts +++ b/src/tools/insights/getSqlInsight.ts @@ -2,41 +2,39 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - query: z - .string() - .max(1000) - .describe( - "Your natural language query describing the SQL insight (max 1000 characters).", - ), + query: z + .string() + .max(1000) + .describe("Your natural language query describing the SQL insight (max 1000 characters)."), }); type Params = z.infer; export const getSqlInsightHandler = async (context: Context, params: Params) => { - const { query } = params; - const projectId = await context.getProjectId(); + const { query } = params; + const projectId = await context.getProjectId(); - const result = await context.api.insights({ projectId }).sqlInsight({ query }); - if (!result.success) { - throw new Error(`Failed to execute SQL insight: ${result.error.message}`); - } + const result = await context.api.insights({ projectId }).sqlInsight({ query }); + if (!result.success) { + throw new Error(`Failed to execute SQL insight: ${result.error.message}`); + } - if (result.data.results.length === 0) { - return { - content: [ - { - type: "text", - text: "Received an empty SQL insight or no data in the stream.", - }, - ], - }; - } - return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; + if (result.data.results.length === 0) { + return { + content: [ + { + type: "text", + text: "Received an empty SQL insight or no data in the stream.", + }, + ], + }; + } + return { content: [{ type: "text", text: JSON.stringify(result.data) }] }; }; const tool = (): Tool => ({ - name: "get-sql-insight", - description: ` + name: "get-sql-insight", + description: ` - Queries project's PostHog data warehouse based on a provided natural language question - don't provide SQL query as input but describe the output you want. - Data warehouse schema includes data like events and persons. - Use this tool to get a quick answer to a question about the data in the project, which can't be answered using other, more dedicated tools. @@ -44,8 +42,8 @@ const tool = (): Tool => ({ - When giving the results back to the user, first show the SQL query that was used, then briefly explain the query, then provide results in reasily readable format. - You should also offer to save the query as an insight if the user wants to. `, - schema, - handler: getSqlInsightHandler, + schema, + handler: getSqlInsightHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/insights/update.ts b/src/tools/insights/update.ts index 9ba1752..0784d59 100644 --- a/src/tools/insights/update.ts +++ b/src/tools/insights/update.ts @@ -4,40 +4,40 @@ import { UpdateInsightInputSchema } from "../../schema/insights"; import { getProjectBaseUrl } from "../../lib/utils/api"; const schema = z.object({ - insightId: z.number(), - data: UpdateInsightInputSchema, + insightId: z.number(), + data: UpdateInsightInputSchema, }); type Params = z.infer; export const updateHandler = async (context: Context, params: Params) => { - const { insightId, data } = params; - const projectId = await context.getProjectId(); - const insightResult = await context.api.insights({ projectId }).update({ - insightId, - data, - }); - - if (!insightResult.success) { - throw new Error(`Failed to update insight: ${insightResult.error.message}`); - } - - const insightWithUrl = { - ...insightResult.data, - url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, - }; - - return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; + const { insightId, data } = params; + const projectId = await context.getProjectId(); + const insightResult = await context.api.insights({ projectId }).update({ + insightId, + data, + }); + + if (!insightResult.success) { + throw new Error(`Failed to update insight: ${insightResult.error.message}`); + } + + const insightWithUrl = { + ...insightResult.data, + url: `${getProjectBaseUrl(projectId)}/insights/${insightResult.data.short_id}`, + }; + + return { content: [{ type: "text", text: JSON.stringify(insightWithUrl) }] }; }; const tool = (): Tool => ({ - name: "insight-update", - description: ` + name: "insight-update", + description: ` - Update an existing insight by ID. - Can update name, description, filters, and other properties. `, - schema, - handler: updateHandler, + schema, + handler: updateHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/llmObservability/getLLMCosts.ts b/src/tools/llmObservability/getLLMCosts.ts index 0c45151..7269b6d 100644 --- a/src/tools/llmObservability/getLLMCosts.ts +++ b/src/tools/llmObservability/getLLMCosts.ts @@ -2,51 +2,49 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - projectId: z.string(), - days: z.number().optional(), + projectId: z.string(), + days: z.number().optional(), }); type Params = z.infer; export const getLLMCostsHandler = async (context: Context, params: Params) => { - const { projectId, days } = params; - - const trendsQuery = { - kind: "TrendsQuery", - dateRange: { - date_from: `-${days || 6}d`, - date_to: null, - }, - filterTestAccounts: true, - series: [ - { - event: "$ai_generation", - name: "$ai_generation", - math: "sum", - math_property: "$ai_total_cost_usd", - kind: "EventsNode", - }, - ], - breakdownFilter: { - breakdown_type: "event", - breakdown: "$ai_model", - }, - }; + const { projectId, days } = params; - const costsResult = await context.api - .query({ projectId }) - .execute({ queryBody: trendsQuery }); - if (!costsResult.success) { - throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`); - } - return { - content: [{ type: "text", text: JSON.stringify(costsResult.data.results) }], - }; + const trendsQuery = { + kind: "TrendsQuery", + dateRange: { + date_from: `-${days || 6}d`, + date_to: null, + }, + filterTestAccounts: true, + series: [ + { + event: "$ai_generation", + name: "$ai_generation", + math: "sum", + math_property: "$ai_total_cost_usd", + kind: "EventsNode", + }, + ], + breakdownFilter: { + breakdown_type: "event", + breakdown: "$ai_model", + }, + }; + + const costsResult = await context.api.query({ projectId }).execute({ queryBody: trendsQuery }); + if (!costsResult.success) { + throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`); + } + return { + content: [{ type: "text", text: JSON.stringify(costsResult.data.results) }], + }; }; const tool = (): Tool => ({ - name: "get-llm-total-costs-for-project", - description: ` + name: "get-llm-total-costs-for-project", + description: ` - Fetches the total LLM daily costs for each model for a project over a given number of days. - If no number of days is provided, it defaults to 7. - The results are sorted by model name. @@ -60,8 +58,8 @@ const tool = (): Tool => ({ - Write in bold the model name with the highest total cost. - Properly render the markdown table in the response. `, - schema, - handler: getLLMCostsHandler, + schema, + handler: getLLMCostsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/organizations/getDetails.ts b/src/tools/organizations/getDetails.ts index 0387076..a88494a 100644 --- a/src/tools/organizations/getDetails.ts +++ b/src/tools/organizations/getDetails.ts @@ -6,28 +6,26 @@ const schema = z.object({}); type Params = z.infer; export const getDetailsHandler = async (context: Context, _params: Params) => { - const orgId = await context.getOrgID(); - - const orgResult = await context.api.organizations().get({ orgId }); - - if (!orgResult.success) { - throw new Error( - `Failed to get organization details: ${orgResult.error.message}`, - ); - } - - return { - content: [{ type: "text", text: JSON.stringify(orgResult.data) }], - }; + const orgId = await context.getOrgID(); + + const orgResult = await context.api.organizations().get({ orgId }); + + if (!orgResult.success) { + throw new Error(`Failed to get organization details: ${orgResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(orgResult.data) }], + }; }; const tool = (): Tool => ({ - name: "organization-details-get", - description: ` + name: "organization-details-get", + description: ` - Use this tool to get the details of the active organization. `, - schema, - handler: getDetailsHandler, + schema, + handler: getDetailsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/organizations/getOrganizations.ts b/src/tools/organizations/getOrganizations.ts index c60cabf..d130b40 100644 --- a/src/tools/organizations/getOrganizations.ts +++ b/src/tools/organizations/getOrganizations.ts @@ -6,23 +6,23 @@ const schema = z.object({}); type Params = z.infer; export const getOrganizationsHandler = async (context: Context, _params: Params) => { - const orgsResult = await context.api.organizations().list(); - if (!orgsResult.success) { - throw new Error(`Failed to get organizations: ${orgsResult.error.message}`); - } - - return { - content: [{ type: "text", text: JSON.stringify(orgsResult.data) }], - }; + const orgsResult = await context.api.organizations().list(); + if (!orgsResult.success) { + throw new Error(`Failed to get organizations: ${orgsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(orgsResult.data) }], + }; }; const tool = (): Tool => ({ - name: "organizations-get", - description: ` + name: "organizations-get", + description: ` - Use this tool to get the organizations the user has access to. `, - schema, - handler: getOrganizationsHandler, + schema, + handler: getOrganizationsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/organizations/setActive.ts b/src/tools/organizations/setActive.ts index c0c7a04..20fcf16 100644 --- a/src/tools/organizations/setActive.ts +++ b/src/tools/organizations/setActive.ts @@ -2,27 +2,27 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - orgId: z.string(), + orgId: z.string(), }); type Params = z.infer; export const setActiveHandler = async (context: Context, params: Params) => { - const { orgId } = params; - await context.cache.set("orgId", orgId); + const { orgId } = params; + await context.cache.set("orgId", orgId); - return { - content: [{ type: "text", text: `Switched to organization ${orgId}` }], - }; + return { + content: [{ type: "text", text: `Switched to organization ${orgId}` }], + }; }; const tool = (): Tool => ({ - name: "organization-set-active", - description: ` + name: "organization-set-active", + description: ` - Use this tool to set the active organization. `, - schema, - handler: setActiveHandler, + schema, + handler: setActiveHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/projects/getProjects.ts b/src/tools/projects/getProjects.ts index 225f146..0d7fe7c 100644 --- a/src/tools/projects/getProjects.ts +++ b/src/tools/projects/getProjects.ts @@ -6,26 +6,26 @@ const schema = z.object({}); type Params = z.infer; export const getProjectsHandler = async (context: Context, _params: Params) => { - const orgId = await context.getOrgID(); - const projectsResult = await context.api.organizations().projects({ orgId }).list(); - - if (!projectsResult.success) { - throw new Error(`Failed to get projects: ${projectsResult.error.message}`); - } + const orgId = await context.getOrgID(); + const projectsResult = await context.api.organizations().projects({ orgId }).list(); - return { - content: [{ type: "text", text: JSON.stringify(projectsResult.data) }], - }; + if (!projectsResult.success) { + throw new Error(`Failed to get projects: ${projectsResult.error.message}`); + } + + return { + content: [{ type: "text", text: JSON.stringify(projectsResult.data) }], + }; }; const tool = (): Tool => ({ - name: "projects-get", - description: ` + name: "projects-get", + description: ` - Fetches projects that the user has access to - the orgId is optional. - Use this tool before you use any other tools (besides organization-* and docs-search) to allow user to select the project they want to use for subsequent requests. `, - schema, - handler: getProjectsHandler, + schema, + handler: getProjectsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/projects/propertyDefinitions.ts b/src/tools/projects/propertyDefinitions.ts index f0155bc..5036763 100644 --- a/src/tools/projects/propertyDefinitions.ts +++ b/src/tools/projects/propertyDefinitions.ts @@ -6,27 +6,25 @@ const schema = z.object({}); type Params = z.infer; export const propertyDefinitionsHandler = async (context: Context, _params: Params) => { - const projectId = await context.getProjectId(); + const projectId = await context.getProjectId(); - const propDefsResult = await context.api.projects().propertyDefinitions({ projectId }); + const propDefsResult = await context.api.projects().propertyDefinitions({ projectId }); - if (!propDefsResult.success) { - throw new Error( - `Failed to get property definitions: ${propDefsResult.error.message}`, - ); - } - return { - content: [{ type: "text", text: JSON.stringify(propDefsResult.data) }], - }; + if (!propDefsResult.success) { + throw new Error(`Failed to get property definitions: ${propDefsResult.error.message}`); + } + return { + content: [{ type: "text", text: JSON.stringify(propDefsResult.data) }], + }; }; const tool = (): Tool => ({ - name: "property-definitions", - description: ` + name: "property-definitions", + description: ` - Use this tool to get the property definitions of the active project. `, - schema, - handler: propertyDefinitionsHandler, + schema, + handler: propertyDefinitionsHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/projects/setActive.ts b/src/tools/projects/setActive.ts index 2b73fd0..51a3326 100644 --- a/src/tools/projects/setActive.ts +++ b/src/tools/projects/setActive.ts @@ -2,28 +2,28 @@ import { z } from "zod"; import type { Context, Tool } from "../types"; const schema = z.object({ - projectId: z.string(), + projectId: z.string(), }); type Params = z.infer; export const setActiveHandler = async (context: Context, params: Params) => { - const { projectId } = params; + const { projectId } = params; - await context.cache.set("projectId", projectId); + await context.cache.set("projectId", projectId); - return { - content: [{ type: "text", text: `Switched to project ${projectId}` }], - }; + return { + content: [{ type: "text", text: `Switched to project ${projectId}` }], + }; }; const tool = (): Tool => ({ - name: "project-set-active", - description: ` + name: "project-set-active", + description: ` - Use this tool to set the active project. `, - schema, - handler: setActiveHandler, + schema, + handler: setActiveHandler, }); -export default tool; \ No newline at end of file +export default tool; diff --git a/src/tools/types.ts b/src/tools/types.ts index eefe8f1..4a4883d 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -1,27 +1,27 @@ -import { z } from "zod"; -import { ApiClient } from "../api/client"; -import { DurableObjectCache } from "../lib/utils/cache/DurableObjectCache"; +import type { z } from "zod"; +import type { ApiClient } from "../api/client"; +import type { DurableObjectCache } from "../lib/utils/cache/DurableObjectCache"; export type State = { - projectId: string | undefined; - orgId: string | undefined; - distinctId: string | undefined; + projectId: string | undefined; + orgId: string | undefined; + distinctId: string | undefined; }; export type Context = { - api: ApiClient; - cache: DurableObjectCache; - env: Env; - getProjectId: () => Promise; - getOrgID: () => Promise; - getDistinctId: () => Promise; + api: ApiClient; + cache: DurableObjectCache; + env: Env; + getProjectId: () => Promise; + getOrgID: () => Promise; + getDistinctId: () => Promise; }; export type Tool = { - name: string; - description: string; - schema: TSchema; - handler: (context: Context, params: z.infer) => Promise; + name: string; + description: string; + schema: TSchema; + handler: (context: Context, params: z.infer) => Promise; }; -export type ZodObjectAny = z.ZodObject; \ No newline at end of file +export type ZodObjectAny = z.ZodObject; From 78e82c2491b333691eee8a02598d62fb0996ef25 Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Tue, 22 Jul 2025 14:45:13 +0200 Subject: [PATCH 3/4] add token validation --- src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/index.ts b/src/index.ts index 5714803..566d5c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,10 @@ export class MyMCP extends McpAgent { } get cache() { + 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, @@ -189,6 +193,12 @@ export default { }); } + if (!token.startsWith("phx_")) { + return new Response("Invalid token, please provide a valid API token.", { + status: 401, + }); + } + ctx.props = { apiToken: token, userHash: hash(token), From aebfe70d916b2aa41486c2a538b60028c429997c Mon Sep 17 00:00:00 2001 From: Jonathan Mieloo <32547391+JonathanLab@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:56:03 +0200 Subject: [PATCH 4/4] Update src/tools/insights/create.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tools/insights/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/insights/create.ts b/src/tools/insights/create.ts index a7082f0..3300d57 100644 --- a/src/tools/insights/create.ts +++ b/src/tools/insights/create.ts @@ -32,7 +32,7 @@ const tool = (): Tool => ({ - If the user wants to see data, you should use the "get-sql-insight" tool to get that data instead. - An insight requires a name, query, and other optional properties. - The query should use HogQL, which is a variant of Clickhouse SQL. Here is an example query: - Here is an example of a validquery: + Here is an example of a valid query: { "kind": "DataVisualizationNode", "source": {