Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"worker-configuration.d.ts",
".mypy_cache/**",
"schema/tool-inputs.json",
"python/**"
"python/**",
"**/generated.ts"
]
},
"vcs": {
Expand Down
5 changes: 4 additions & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"test": "vitest",
"test:integration": "vitest run --config vitest.integration.config.mts",
"test:watch": "vitest watch",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"generate-client": "tsx scripts/update-openapi-client.ts"
},
"keywords": ["posthog", "mcp", "ai", "agents", "analytics", "feature-flags"],
"author": "PostHog Inc.",
Expand All @@ -62,6 +63,8 @@
"dotenv": "^16.4.7",
"langchain": "^0.3.31",
"tsup": "^8.5.0",
"tsx": "^4.20.5",
"typed-openapi": "^2.2.2",
"typescript": "^5.8.3",
"uuid": "^11.1.0",
"vite": "^5.0.0",
Expand Down
518 changes: 514 additions & 4 deletions typescript/pnpm-lock.yaml

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions typescript/scripts/update-openapi-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env tsx

import { execSync } from "node:child_process";
import * as fs from "node:fs";

const SCHEMA_URL = "https://app.posthog.com/api/schema/";
const TEMP_SCHEMA_PATH = "temp-openapi.yaml";
const OUTPUT_PATH = "src/api/generated.ts";

async function fetchSchema() {
console.log("Fetching OpenAPI schema from PostHog API...");

try {
const response = await fetch(SCHEMA_URL);
if (!response.ok) {
throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`);
}

const schema = await response.text();
fs.writeFileSync(TEMP_SCHEMA_PATH, schema, "utf-8");
console.log(`✓ Schema saved to ${TEMP_SCHEMA_PATH}`);

return true;
} catch (error) {
console.error("Error fetching schema:", error);
return false;
}
}

function generateClient() {
console.log("Generating TypeScript client...");

try {
execSync(`pnpm typed-openapi ${TEMP_SCHEMA_PATH} --output ${OUTPUT_PATH}`, {
stdio: "inherit",
});
console.log(`✓ Client generated at ${OUTPUT_PATH}`);
return true;
} catch (error) {
console.error("Error generating client:", error);
return false;
}
}

function cleanup() {
try {
if (fs.existsSync(TEMP_SCHEMA_PATH)) {
fs.unlinkSync(TEMP_SCHEMA_PATH);
console.log("✓ Cleaned up temporary schema file");
}
} catch (error) {
console.error("Warning: Could not clean up temporary file:", error);
}
}

async function main() {
console.log("Starting OpenAPI client update...\n");

const schemaFetched = await fetchSchema();
if (!schemaFetched) {
process.exit(1);
}

const clientGenerated = generateClient();

cleanup();

if (!clientGenerated) {
process.exit(1);
}

console.log("\n✅ OpenAPI client successfully updated!");
}

main().catch((error) => {
console.error("Unexpected error:", error);
process.exit(1);
});
44 changes: 25 additions & 19 deletions typescript/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import {
SurveyResponseStatsOutputSchema,
UpdateSurveyInputSchema,
} from "../schema/surveys.js";
import { buildApiFetcher } from "./fetcher";
import { type Schemas, type TypedErrorResponse, createApiClient } from "./generated";

export type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E };

Expand All @@ -71,10 +73,14 @@ export interface ApiConfig {
export class ApiClient {
private config: ApiConfig;
private baseUrl: string;
// NOTE: The OpenAPI schema for the generated client is not always accurate
public generated: ReturnType<typeof createApiClient>;

constructor(config: ApiConfig) {
this.config = config;
this.baseUrl = config.baseUrl;

this.generated = createApiClient(buildApiFetcher(this.config), this.baseUrl);
}
private buildHeaders() {
return {
Expand Down Expand Up @@ -487,27 +493,27 @@ export class ApiClient {
return {
list: async ({
params,
}: { params?: ListInsightsData } = {}): Promise<Result<Array<SimpleInsight>>> => {
const validatedParams = params ? ListInsightsSchema.parse(params) : undefined;
const searchParams = new URLSearchParams();

if (validatedParams?.limit)
searchParams.append("limit", String(validatedParams.limit));
if (validatedParams?.offset)
searchParams.append("offset", String(validatedParams.offset));
if (validatedParams?.search) searchParams.append("search", validatedParams.search);

const url = `${this.baseUrl}/api/projects/${projectId}/insights/${searchParams.toString() ? `?${searchParams}` : ""}`;

const responseSchema = z.object({
results: z.array(SimpleInsightSchema),
});
}: { params?: ListInsightsData } = {}): Promise<Result<Array<Schemas.Insight>>> => {
try {
const response = await this.generated.get(
"/api/projects/{project_id}/insights/",
{
path: { project_id: projectId },
query: params
? {
limit: params.limit,
offset: params.offset,
//@ts-expect-error search is not implemented as a query parameter
search: params.search,
}
: {},
},
);

const result = await this.fetchWithSchema(url, responseSchema);
if (result.success) {
return { success: true, data: result.data.results };
return { success: true, data: response.results };
} catch (error) {
return { success: false, error: error as Error };
Comment thread
joshsny marked this conversation as resolved.
}
return result;
},

create: async ({
Expand Down
52 changes: 52 additions & 0 deletions typescript/src/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ApiConfig } from "./client";
import type { createApiClient } from "./generated";

export const buildApiFetcher: (config: ApiConfig) => Parameters<typeof createApiClient>[0] = (
config,
) => {
return {
fetch: async (input) => {
const headers = new Headers();
headers.set("Authorization", `Bearer ${config.apiToken}`);

// Handle query parameters
if (input.urlSearchParams) {
input.url.search = input.urlSearchParams.toString();
}

// Handle request body for mutation methods
const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase())
? JSON.stringify(input.parameters?.body)
: undefined;

if (body) {
headers.set("Content-Type", "application/json");
}

// Add custom headers
if (input.parameters?.header) {
for (const [key, value] of Object.entries(input.parameters.header)) {
if (value != null) {
headers.set(key, String(value));
}
}
}

const response = await fetch(input.url, {
method: input.method.toUpperCase(),
...(body && { body }),
headers,
...input.overrides,
});

if (!response.ok) {
const errorResponse = await response.json();
throw new Error(
`Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`,
);
}

return response;
},
};
};
Loading