Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/corsair/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { CorsairClientError, createCorsairClient } from './client';
export type { ResolveConnectLinkResult } from './core';
export {
AuthMissingError,
assertReadonlyAllowed,
createCorsair,
PermissionRequiredError,
ReadonlyForbiddenError,
Expand Down
240 changes: 238 additions & 2 deletions packages/mcp/src/core/tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import type { AnyCorsairInstance } from 'corsair';
import { listOperations, runReadonly } from 'corsair';
import { assertReadonlyAllowed, listOperations, runReadonly } from 'corsair';
import { z } from 'zod';
import type { BaseMcpOptions } from './adapters.js';
import { formatGetSchemaResponse } from './schema-format.js';
Expand All @@ -22,6 +22,241 @@ export type CorsairToolDef = {
handler: (args: Record<string, unknown>) => Promise<CallToolResult>;
};

export function createScopedCorsairProxy(corsairObj: any): any {
if (!corsairObj || typeof corsairObj !== 'object') return corsairObj;

const wrapManage = (value: any) => {
if (!value) return value;
return new Proxy(value, {
get(manageTarget, manageProp, manageReceiver) {
return wrapManageProp(
Reflect.get(manageTarget, manageProp, manageReceiver),
manageProp,
);
},
getOwnPropertyDescriptor(manageTarget, manageProp) {
const desc = Reflect.getOwnPropertyDescriptor(manageTarget, manageProp);
if (desc && 'value' in desc) {
desc.value = wrapManageProp(desc.value, manageProp);
}
return desc;
},
});
};

const wrapManageProp = (manageValue: any, manageProp: string | symbol) => {
if (manageProp === 'tenants') {
if (!manageValue) return manageValue;
return new Proxy(manageValue, {
get(tTarget, tProp, tReceiver) {
return wrapTenantProp(Reflect.get(tTarget, tProp, tReceiver), tProp);
},
getOwnPropertyDescriptor(tTarget, tProp) {
const desc = Reflect.getOwnPropertyDescriptor(tTarget, tProp);
if (desc && 'value' in desc) {
desc.value = wrapTenantProp(desc.value, tProp);
}
return desc;
},
});
}

if (manageProp === 'connect') {
if (!manageValue) return manageValue;
return new Proxy(manageValue, {
get(cTarget, cProp, cReceiver) {
return wrapConnectProp(Reflect.get(cTarget, cProp, cReceiver), cProp);
},
getOwnPropertyDescriptor(cTarget, cProp) {
const desc = Reflect.getOwnPropertyDescriptor(cTarget, cProp);
if (desc && 'value' in desc) {
desc.value = wrapConnectProp(desc.value, cProp);
}
return desc;
},
});
}

return manageValue;
};

const wrapTenantProp = (methodValue: any, tProp: string | symbol) => {
if (tProp === 'create') {
return function () {
throw new Error(
'manage.tenants.create is not available in run_script.',
);
};
}
return methodValue;
};

const wrapConnectProp = (methodValue: any, cProp: string | symbol) => {
if (cProp === 'createLink') {
return function () {
throw new Error(
'manage.connect.createLink is not available in run_script.',
);
};
}
if (cProp === 'oauthCallback') {
return function () {
throw new Error(
'manage.connect.oauthCallback is not available in run_script.',
);
};
}
return methodValue;
};

const wrapKeys = (value: any) => {
return new Proxy(value || {}, {
get() {
throw new Error(
'Credential access (keys) not available in run_script. Use corsair.<plugin>.api.* endpoints instead.',
);
},
getOwnPropertyDescriptor() {
throw new Error(
'Credential access (keys) not available in run_script. Use corsair.<plugin>.api.* endpoints instead.',
);
},
});
};

const wrapPluginProp = (pluginValue: any, pluginProp: string | symbol) => {
if (pluginProp === 'keys') {
return wrapKeys(pluginValue);
}

if (pluginProp === 'db') {
if (!pluginValue) return pluginValue;
return new Proxy(pluginValue, {
get(dbTarget, dbProp, dbReceiver) {
return wrapDbProp(Reflect.get(dbTarget, dbProp, dbReceiver), dbProp);
},
getOwnPropertyDescriptor(dbTarget, dbProp) {
const desc = Reflect.getOwnPropertyDescriptor(dbTarget, dbProp);
if (desc && 'value' in desc) {
desc.value = wrapDbProp(desc.value, dbProp);
}
return desc;
},
});
}

return pluginValue;
};

const wrapDbProp = (entityValue: any, dbProp: string | symbol) => {
if (entityValue && typeof entityValue === 'object') {
return new Proxy(entityValue, {
get(eTarget, eProp, eReceiver) {
return wrapEntityMethod(
Reflect.get(eTarget, eProp, eReceiver),
eProp,
eTarget,
);
},
getOwnPropertyDescriptor(eTarget, eProp) {
const desc = Reflect.getOwnPropertyDescriptor(eTarget, eProp);
if (desc && 'value' in desc) {
desc.value = wrapEntityMethod(desc.value, eProp, eTarget);
}
return desc;
},
});
}
return entityValue;
};

const wrapEntityMethod = (
methodValue: any,
eProp: string | symbol,
eTarget: any,
) => {
if (typeof methodValue === 'function') {
if (eProp === 'upsertByEntityId') {
return function (...args: any[]) {
assertReadonlyAllowed('db.upsertByEntityId', 'write');
return methodValue.apply(eTarget, args);
};
}
if (eProp === 'deleteById') {
return function (...args: any[]) {
assertReadonlyAllowed('db.deleteById', 'write');
return methodValue.apply(eTarget, args);
};
}
if (eProp === 'deleteByEntityId') {
return function (...args: any[]) {
assertReadonlyAllowed('db.deleteByEntityId', 'write');
return methodValue.apply(eTarget, args);
};
}
}
return methodValue;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const wrapProp = (value: any, prop: string | symbol, target: any) => {
if (prop === 'withTenant' && typeof value === 'function') {
return function (...args: any[]) {
const tenantClient = value.apply(target, args);
return createScopedCorsairProxy(tenantClient);
};
}

if (prop === 'manage') {
return wrapManage(value);
}

if (prop === 'permissions') {
return undefined;
}

if (prop === 'keys') {
return wrapKeys(value);
}

if (value && typeof value === 'object') {
return new Proxy(value, {
get(pluginTarget, pluginProp, pluginReceiver) {
return wrapPluginProp(
Reflect.get(pluginTarget, pluginProp, pluginReceiver),
pluginProp,
);
},
getOwnPropertyDescriptor(pluginTarget, pluginProp) {
const desc = Reflect.getOwnPropertyDescriptor(
pluginTarget,
pluginProp,
);
if (desc && 'value' in desc) {
desc.value = wrapPluginProp(desc.value, pluginProp);
}
return desc;
},
});
}

return value;
};

return new Proxy(corsairObj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return wrapProp(value, prop, target);
},
getOwnPropertyDescriptor(target, prop) {
const desc = Reflect.getOwnPropertyDescriptor(target, prop);
if (desc && 'value' in desc) {
desc.value = wrapProp(desc.value, prop, target);
}
return desc;
},
});
Comment thread
loveyadav1015 marked this conversation as resolved.
}

export function buildCorsairToolDefs(
options: BaseMcpOptions,
): CorsairToolDef[] {
Expand Down Expand Up @@ -87,12 +322,13 @@ export function buildCorsairToolDefs(
handler: async ({ code }) => {
const readonly = runOptions?.readonly || false;
try {
const scopedCorsair = createScopedCorsairProxy(corsair);
const fn = new Function(
'corsair',
`return (async () => { ${code} })()`,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const invoke = () =>
(fn as (c: unknown) => Promise<unknown>)(corsair);
(fn as (c: unknown) => Promise<unknown>)(scopedCorsair);
// When readonly is required, run the whole script inside a readonly
// scope that takes precedence over the developer's permission config.
// Any write/destructive endpoint throws and aborts the script.
Expand Down
Loading
Loading