Skip to content
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
157 changes: 157 additions & 0 deletions src/__tests__/server.processUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { applyStaticProperty, getSetMemoKey } from '../server.processUser';

describe('applyStaticProperty', () => {
it.each([
{
description: 'object',
obj: {},
value: 'lorem',
name: 'ipsum',
expected: true
},
{
description: 'function',
obj: () => {},
value: 'lorem',
name: 'ipsum',
expected: true
}
])('should apply a property, $description', ({ name, value, obj, expected }) => {
const result = applyStaticProperty(name, value, obj);

expect(result).toBe(expected);
expect((obj as any)?.[name]).toBe(value);

const descriptor = Object.getOwnPropertyDescriptor(obj, name);

expect(descriptor).toBeDefined();
expect(descriptor?.value).toBe(value);
expect(descriptor?.writable).toBe(false);
expect(descriptor?.enumerable).toBe(false);
expect(descriptor?.configurable).toBe(false);
});

it.each([
{
description: 'null',
obj: null,
value: 'lorem',
name: 'ipsum',
expected: false
},
{
description: 'undefined',
obj: undefined,
value: 'lorem',
name: 'ipsum',
expected: false
}
])('should fail to apply a property, $description', ({ name, value, obj, expected }) => {
const result = applyStaticProperty(name, value, obj);

expect(result).toBe(expected);
});

it('should return false when trying to overwrite a non-configurable property', () => {
const obj = {};

Object.defineProperty(obj, 'fixedProp', {
value: 'original',
writable: false,
configurable: false
});

const result = applyStaticProperty('fixedProp', 'newVal', obj);

expect(result).toBe(false);
});
});

describe('getSetMemoKey', () => {
it.each([
{
description: 'string',
input: 'myString',
contextKey: 'ctx',
expected: 'myString:ctx'
},
{
description: 'number',
input: 42,
contextKey: 'ctx',
expected: '42:ctx'
},
{
description: 'boolean',
input: true,
contextKey: 'ctx',
expected: 'true:ctx'
},
{
description: 'null',
input: null,
contextKey: 'ctx',
expected: 'null:ctx'
},
{
description: 'undefined',
input: undefined,
contextKey: 'ctx',
expected: 'undefined:ctx'
}
])('should return format "input:contextKey", $description', ({ input, contextKey, expected }) => {
const result = getSetMemoKey(input, contextKey);

expect(result).toBe(expected);
});

it('should handle symbol primitives by converting to string representation', () => {
const sym = Symbol('testSym');
const result = getSetMemoKey(sym, 'ctx');

expect(result).toBe('Symbol(testSym):ctx');
});

it.each([
{
description: 'object',
input: {}
},
{
description: 'function',
input: () => {}
}
])('should return a Symbol with plugins:contextKey description, $description', ({ input }) => {
const result = getSetMemoKey(input, 'someCtx');

expect(typeof result).toBe('symbol');
expect((result as symbol).description).toBe('plugins:someCtx');
});

it.each([
{
description: 'object',
createRef: () => ({})
},
{
description: 'function',
createRef: () => () => {}
}
])('should handle uniqueness and memoization for references, $description', ({ createRef }) => {
const ref1 = createRef();
const ref2 = createRef();

const token1 = getSetMemoKey(ref1, 'myKey');
const token2 = getSetMemoKey(ref1, 'myKey');

expect(token1).toBe(token2);

const token3 = getSetMemoKey(ref2, 'myKey');

expect(token1).not.toBe(token3);

const token4 = getSetMemoKey(ref1, 'otherKey');

expect(token1).not.toBe(token4);
});
});
61 changes: 61 additions & 0 deletions src/server.processUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { isReferenceLike } from './server.helpers';

/**
* Apply a static property to an object.
*
* @param property - Name of the property to apply
* @param value - Value of the property to apply
* @param obj - Object to apply the property towards
* @returns `true` if the property was applied successfully, `false` otherwise.
*/
const applyStaticProperty = (property: string, value: unknown, obj: unknown) => {
try {
Object.defineProperty(obj, property, { value, writable: false, enumerable: false, configurable: false });
} catch {
return false;
}

return true;
};

/**
* Memoization key store. See `getSetMemoKey`.
*/
const pluginMemoKeyStore: WeakMap<object, Map<string, symbol>> = new WeakMap();

/**
* Quick consistent unique key, via symbol (anything unique-like will work), for a given input
* and context.
*
* Used specifically for helping memoize functions and objects against context. Not used
* elsewhere because simple equality checks, without context, in the lower-level functions
* are good enough.
*
* @param input - Input can be an object, function, or primitive value.
* @param contextKey - Additional context to help uniqueness.
* @returns A unique key, a symbol for objects/functions or string for primitives.
*/
const getSetMemoKey = (input: unknown, contextKey: string) => {
if (!isReferenceLike(input)) {
return `${String(input)}:${contextKey}`;
}

let contextMap = pluginMemoKeyStore.get(input);
let token;

if (!contextMap) {
contextMap = new Map<string, symbol>();
pluginMemoKeyStore.set(input, contextMap);
}

token = contextMap.get(contextKey);

if (!token) {
token = Symbol(`plugins:${contextKey}`);
contextMap.set(contextKey, token);
}

return token;
};

export { applyStaticProperty, getSetMemoKey };
6 changes: 6 additions & 0 deletions src/server.toolsHostCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { type McpTool, type McpToolCreator } from './mcpSdk';
/**
* Apply a static property to an object.
*
* @note **Do not import the centralized helper from `server.processUser.ts`.**
* This duplication is intentional. This file should stay dependency-minimal.
* Importing shared helpers would pull in parent-side modules and widen the
* dependency graph, busting the lightweight child-process environment
* constraints.
*
* @private
* @param property - Name of the property to apply
* @param value - Value of the property to apply
Expand Down
67 changes: 4 additions & 63 deletions src/server.toolsUser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, isAbsolute, resolve } from 'node:path';
import { isPath, isPlainObject, isReferenceLike, isUrl } from './server.helpers';
import { type McpTool } from './mcpSdk';
import { type GlobalOptions } from './options';
import { isPath, isPlainObject, isReferenceLike, isUrl } from './server.helpers';
import { memo } from './server.caching';
import { applyStaticProperty, getSetMemoKey } from './server.processUser';
import { normalizeInputSchema } from './server.schema';
import { type GlobalOptions } from './options';
import { DEFAULT_OPTIONS } from './options.defaults';
import { type ToolOptions } from './options.tools';
import { formatUnknownError } from './logger';
import { normalizeInputSchema } from './server.schema';

/**
* Inline tool options.
Expand Down Expand Up @@ -204,66 +205,6 @@ const ALLOWED_CONFIG_KEYS = new Set(['name', 'description', 'inputSchema', 'hand
*/
const ALLOWED_SCHEMA_KEYS = new Set(['description', 'inputSchema']);

/**
* Memoization key store. See `getSetMemoKey`.
*/
const toolsMemoKeyStore: WeakMap<object, Map<string, symbol>> = new WeakMap();

/**
* Quick consistent unique key, via symbol (anything unique-like will work), for a given input
* and context.
*
* Used specifically for helping memoize functions and objects against context. Not used
* elsewhere because simple equality checks, without context, in the lower-level functions
* are good enough.
*
* @private
* @param input - Input can be an object, function, or primitive value.
* @param contextKey - Additional context to help uniqueness.
* @returns A unique key, a symbol for objects/functions or string for primitives.
*/
const getSetMemoKey = (input: unknown, contextKey: string) => {
if (!isReferenceLike(input)) {
return `${String(input)}:${contextKey}`;
}

let contextMap = toolsMemoKeyStore.get(input);
let token;

if (!contextMap) {
contextMap = new Map<string, symbol>();
toolsMemoKeyStore.set(input, contextMap);
}

token = contextMap.get(contextKey);

if (!token) {
token = Symbol(`tools:${contextKey}`);
contextMap.set(contextKey, token);
}

return token;
};

/**
* Apply a static property to an object.
*
* @private
* @param property - Name of the property to apply
* @param value - Value of the property to apply
* @param obj - Object to apply the property towards
* @returns `true` if the property was applied successfully, `false` otherwise.
*/
const applyStaticProperty = (property: string, value: unknown, obj: unknown) => {
try {
Object.defineProperty(obj, property, { value, writable: false, enumerable: false, configurable: false });
} catch {
return false;
}

return true;
};

/**
* Return an object key value.
*
Expand Down
Loading