Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
350 changes: 348 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,351 @@ export type CorsairToolDef = {
handler: (args: Record<string, unknown>) => Promise<CallToolResult>;
};

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

const isRestrictedProp = (prop: string | symbol) =>
prop === '__proto__' || prop === 'constructor' || prop === 'prototype';

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

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

if (manageProp === 'plugins') {
if (!manageValue) return manageValue;
return new Proxy(manageValue, {
get(pTarget, pProp, pReceiver) {
if (isRestrictedProp(pProp)) return undefined;
return wrapPluginManagerProp(
Reflect.get(pTarget, pProp, pReceiver),
pProp,
pTarget,
);
},
getOwnPropertyDescriptor(pTarget, pProp) {
if (isRestrictedProp(pProp)) return undefined;
const desc = Reflect.getOwnPropertyDescriptor(pTarget, pProp);
if (desc && 'value' in desc) {
desc.value = wrapPluginManagerProp(desc.value, pProp, pTarget);
}
return desc;
},
});
}

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

if (typeof manageValue === 'function') {
return function (...args: any[]) {
return manageValue.apply(manageTarget, args);
};
}

return manageValue;
};

const wrapTenantProp = (
methodValue: any,
tProp: string | symbol,
tTarget: any,
) => {
if (typeof methodValue === 'function') {
if (tProp !== 'list' && tProp !== 'get') {
return function () {
throw new Error(
`manage.tenants.${String(tProp)} is not available in run_script.`,
);
};
}
return function (...args: any[]) {
return methodValue.apply(tTarget, args);
};
}
return methodValue;
};

const wrapPluginManagerProp = (
methodValue: any,
pProp: string | symbol,
pTarget: any,
) => {
if (typeof methodValue === 'function') {
if (pProp !== 'list') {
return function () {
throw new Error(
`manage.plugins.${String(pProp)} is not available in run_script.`,
);
};
}
return function (...args: any[]) {
return methodValue.apply(pTarget, args);
};
}
return methodValue;
};

const wrapConnectProp = (
methodValue: any,
cProp: string | symbol,
cTarget: any,
) => {
if (typeof methodValue === 'function') {
return function () {
throw new Error(
`manage.connect.${String(cProp)} 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,
pluginTarget: any,
) => {
if (pluginProp === 'keys') {
return wrapKeys(pluginValue);
}

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

if (typeof pluginValue === 'function') {
return function (...args: any[]) {
return pluginValue.apply(pluginTarget, args);
};
}

return pluginValue;
};

const wrapDbProp = (
entityValue: any,
dbProp: string | symbol,
dbTarget: any,
) => {
if (entityValue && typeof entityValue === 'object') {
return new Proxy(entityValue, {
get(eTarget, eProp, eReceiver) {
if (isRestrictedProp(eProp)) return undefined;
return wrapEntityMethod(
Reflect.get(eTarget, eProp, eReceiver),
eProp,
eTarget,
);
},
getOwnPropertyDescriptor(eTarget, eProp) {
if (isRestrictedProp(eProp)) return undefined;
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') {
const allowedReads = [
'findByEntityId',
'existsByEntityId',
'findIdByEntityId',
'findById',
'findManyByEntityIds',
'list',
'search',
'count',
];
if (!allowedReads.includes(String(eProp))) {
return function (...args: any[]) {
assertReadonlyAllowed(`db.${String(eProp)}`, 'write');
return methodValue.apply(eTarget, args);
};
}
return function (...args: any[]) {
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) {
if (isRestrictedProp(pluginProp)) return undefined;
return wrapPluginProp(
Reflect.get(pluginTarget, pluginProp, pluginReceiver),
pluginProp,
pluginTarget,
);
},
getOwnPropertyDescriptor(pluginTarget, pluginProp) {
if (isRestrictedProp(pluginProp)) return undefined;
const desc = Reflect.getOwnPropertyDescriptor(
pluginTarget,
pluginProp,
);
if (desc && 'value' in desc) {
desc.value = wrapPluginProp(desc.value, pluginProp, pluginTarget);
}
return desc;
},
});
}

if (typeof value === 'function') {
return function (...args: any[]) {
return value.apply(target, args);
};
}

return value;
};

return new Proxy(corsairObj, {
get(target, prop, receiver) {
if (isRestrictedProp(prop)) return undefined;
const value = Reflect.get(target, prop, receiver);
return wrapProp(value, prop, target);
},
getOwnPropertyDescriptor(target, prop) {
if (isRestrictedProp(prop)) return undefined;
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 +432,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