convex
Version:
Client for the Convex Cloud
125 lines (110 loc) • 3.57 kB
text/typescript
import { BigBrainAuth, Context, ErrorType } from "../../../bundler/context.js";
import { Filesystem, nodeFs } from "../../../bundler/fs.js";
import { Ora } from "ora";
import {
DeploymentSelectionWithinProject,
deploymentSelectionWithinProjectSchema,
DeploymentSelectionOptions,
} from "../api.js";
import { z } from "zod";
export interface McpOptions extends DeploymentSelectionOptions {
projectDir?: string;
disableTools?: string;
disableProductionDeployments?: boolean;
}
export class RequestContext implements Context {
fs: Filesystem;
deprecationMessagePrinted = false;
spinner: Ora | undefined;
_cleanupFns: Record<string, (exitCode: number, err?: any) => Promise<void>> =
{};
_bigBrainAuth: BigBrainAuth | null = null;
constructor(public options: McpOptions) {
this.fs = nodeFs;
this.deprecationMessagePrinted = false;
}
async crash(args: {
exitCode: number;
errorType?: ErrorType;
errForSentry?: any;
printedMessage: string | null;
}): Promise<never> {
const cleanupFns = this._cleanupFns;
this._cleanupFns = {};
for (const fn of Object.values(cleanupFns)) {
await fn(args.exitCode, args.errForSentry);
}
// eslint-disable-next-line no-restricted-syntax
throw new RequestCrash(args.exitCode, args.errorType, args.printedMessage);
}
flushAndExit() {
// eslint-disable-next-line no-restricted-syntax
throw new Error("Not implemented");
}
registerCleanup(fn: (exitCode: number, err?: any) => Promise<void>): string {
const handle = crypto.randomUUID();
this._cleanupFns[handle] = fn;
return handle;
}
removeCleanup(handle: string) {
const value = this._cleanupFns[handle];
delete this._cleanupFns[handle];
return value ?? null;
}
bigBrainAuth(): BigBrainAuth | null {
return this._bigBrainAuth;
}
_updateBigBrainAuth(auth: BigBrainAuth | null): void {
this._bigBrainAuth = auth;
}
async decodeDeploymentSelector(encoded: string) {
const { projectDir, deployment } = decodeDeploymentSelector(encoded);
if (
deployment.kind === "prod" &&
this.options.disableProductionDeployments
) {
return await this.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
"Production deployments are disabled due to the --disable-production-deployments flag.",
});
}
return { projectDir, deployment };
}
get productionDeploymentsDisabled() {
return !!this.options.disableProductionDeployments;
}
}
export class RequestCrash {
printedMessage: string;
constructor(
private exitCode: number,
private errorType: ErrorType | undefined,
printedMessage: string | null,
) {
this.printedMessage = printedMessage ?? "Unknown error";
}
}
// Unfortunately, MCP clients don't seem to handle nested JSON objects very
// well (even though this is within spec). To work around this, encode the
// deployment selectors as an obfuscated string that the MCP client can
// opaquely pass around.
export function encodeDeploymentSelector(
projectDir: string,
deployment: DeploymentSelectionWithinProject,
) {
const payload = {
projectDir,
deployment,
};
return `${deployment.kind}:${btoa(JSON.stringify(payload))}`;
}
const payloadSchema = z.object({
projectDir: z.string(),
deployment: deploymentSelectionWithinProjectSchema,
});
function decodeDeploymentSelector(encoded: string) {
const [_, serializedPayload] = encoded.split(":");
return payloadSchema.parse(JSON.parse(atob(serializedPayload)));
}