Skip to main content
Glama

Convex MCP server

Official
by get-convex
send-plain-message.ts8.23 kB
import { PlainClient, ThreadFieldSchemaType, UpsertCustomerInput, } from "@team-plain/typescript-sdk"; import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; import { captureException, captureMessage } from "@sentry/nextjs"; import { Team, ProjectDetails, DeploymentResponse } from "generatedApi"; import { retryingFetch } from "lib/ssr"; const apiKey = process.env.PLAIN_API_KEY; if (!apiKey) { throw new Error("PLAIN_API_KEY environment variable is not set"); } const client = new PlainClient({ apiKey, }); export type ResponseData = { error: string | null; }; const RequestBodySchema = z.object({ subject: z.string(), message: z.string(), teamId: z.number(), projectId: z.number().optional(), deploymentName: z.string().optional(), }); export default async function handler( req: NextApiRequest, res: NextApiResponse<ResponseData>, ) { if (req.headers["x-plain-api-key"] !== process.env.PLAIN_API_KEY) { return res.status(401).json({ error: "Unauthorized" }); } let body: z.infer<typeof RequestBodySchema>; try { body = RequestBodySchema.parse(req.body); } catch (error: any) { return res.status(400).json({ error: error.message }); } try { const profileDataResp = await retryingFetch( `${process.env.NEXT_PUBLIC_BIG_BRAIN_URL}/api/dashboard/profile`, { headers: { authorization: `Bearer ${req.headers["x-convex-access-token"]}`, }, }, ); if (!profileDataResp.ok) { const responseText = await profileDataResp.text(); throw new Error(`Couldn't fetch profile data: ${responseText}`); } const { id, email: profileEmail, name: profileName, } = await profileDataResp.json(); const memberDataResp = await retryingFetch( `${process.env.NEXT_PUBLIC_BIG_BRAIN_URL}/api/dashboard/member_data`, { headers: { authorization: `Bearer ${req.headers["x-convex-access-token"]}`, }, }, ); if (!memberDataResp.ok) { const responseText = await memberDataResp.text(); throw new Error(`Couldn't fetch member data: ${responseText}`); } const { teams, projects, deployments, }: { teams: Team[]; projects: ProjectDetails[]; deployments: DeploymentResponse[]; } = await memberDataResp.json(); const { teamId, projectId, deploymentName } = body; let customerId: string | null = null; const upsertCustomerRes = await upsertPlainCustomer( { externalId: id.toString(), }, id, profileEmail, profileName, ); if (upsertCustomerRes.error) { const customerAlreadyExists = upsertCustomerRes.error.type === "mutation_error" && upsertCustomerRes.error.errorDetails.code === "customer_already_exists_with_email"; if (customerAlreadyExists) { const upsertCustomerWithEmailIdentifierRes = await upsertPlainCustomer( { emailAddress: profileEmail }, id, profileEmail, profileName, ); if (upsertCustomerWithEmailIdentifierRes.error) { throw new Error( `Failed to upsert customer: ${upsertCustomerWithEmailIdentifierRes.error.message}`, ); } customerId = upsertCustomerWithEmailIdentifierRes.data.customer.id; } else { throw new Error( `Failed to upsert customer: ${upsertCustomerRes.error.message}`, ); } } else { customerId = upsertCustomerRes.data.customer.id; } for (const team of teams) { await upsertPlainTenant( client, team, req.headers["x-convex-access-token"] as string, ); } await setPlainCustomerTenants(client, customerId, teams); const team = teams.find((t) => t.id === teamId); const project = projects.find((p) => p.id === projectId); const deployment = deployments.find((d) => d.name === deploymentName); const threadFields: Array<{ key: string; stringValue: string; type: ThreadFieldSchemaType; }> = []; if (team) { threadFields.push({ key: "team_id", stringValue: team.id.toString(), type: ThreadFieldSchemaType.String, }); threadFields.push({ key: "team_slug", stringValue: team.slug, type: ThreadFieldSchemaType.String, }); } if (project) { threadFields.push({ key: "project_id", stringValue: project.id.toString(), type: ThreadFieldSchemaType.String, }); threadFields.push({ key: "project_slug", stringValue: project.slug, type: ThreadFieldSchemaType.String, }); } if (deployment) { threadFields.push({ key: "deployment_name", stringValue: deployment.name, type: ThreadFieldSchemaType.String, }); } const createThreadRes = await client.createThread({ customerIdentifier: { customerId, }, title: body.subject, threadFields, components: [ { componentText: { text: body.message, }, }, ], }); if (createThreadRes.error) { throw new Error( `Failed to create thread: ${createThreadRes.error.message}`, ); } res.status(200).json({ error: null }); } catch (error: any) { captureException(error, { extra: { requestBody: body, }, }); return res.status(500).json({ error: "Internal Server Error" }); } } function upsertPlainCustomer( customerIdentifier: UpsertCustomerInput["identifier"], memberId: number, profileEmail: string, profileName: string | null, ) { return client.upsertCustomer({ identifier: customerIdentifier, onCreate: { fullName: profileName || profileEmail, externalId: memberId.toString(), email: { email: profileEmail, isVerified: true, }, }, onUpdate: { email: { email: profileEmail, isVerified: true, }, fullName: { value: profileName || profileEmail, }, }, }); } async function upsertPlainTenant( plainClient: PlainClient, team: Team, accessToken: string, ) { const upsertTenantRes = await plainClient.upsertTenant({ identifier: { externalId: team.id.toString(), }, externalId: team.id.toString(), name: team.name, url: { value: null }, }); if (upsertTenantRes.error) { captureMessage( `Couldn't upsert tenant: ${upsertTenantRes.error.message}`, "error", ); return; } const subscriptionResp = await retryingFetch( `${process.env.NEXT_PUBLIC_BIG_BRAIN_URL}/api/dashboard/teams/${team.id}/get_orb_subscription`, { headers: { authorization: `Bearer ${accessToken}`, }, }, ); if (!subscriptionResp.ok) { const responseText = await subscriptionResp.text(); captureMessage(`Couldn't fetch subscription: ${responseText}`, "error"); return; } let tier = "CONVEX_BASE"; try { const subscription = await subscriptionResp.json(); if (subscription && subscription.status === "active") { const { planType } = subscription.plan; tier = planType; } } catch (error) { // Do nothing } const updateTenantTierRes = await plainClient.updateTenantTier({ tenantIdentifier: { externalId: team.id.toString(), }, tierIdentifier: { externalId: tier, }, }); if (updateTenantTierRes.error) { captureMessage( `Couldn't update tenant tier: ${updateTenantTierRes.error.message}`, "error", ); } } async function setPlainCustomerTenants( plainClient: PlainClient, customerId: string, teams: Team[], ) { const setCustomerTenantsRes = await plainClient.setCustomerTenants({ customerIdentifier: { customerId, }, tenantIdentifiers: teams.map((t) => ({ externalId: t.id.toString(), })), }); if (setCustomerTenantsRes.error) { captureMessage( `Couldn't set customer tenants: ${setCustomerTenantsRes.error.message}`, "error", ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server