Skip to content

Commit 6f886a3

Browse files
committed
feat: nodejs assertions base
1 parent ed9a75c commit 6f886a3

2 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import {
2+
assertInput,
3+
assertInputString,
4+
assertInputStringLength,
5+
assertInputStringArrayEntryLength,
6+
assertInputStringNumberEnumLike
7+
} from '../server.assertions';
8+
9+
describe('assertInput', () => {
10+
it.each([
11+
{
12+
description: 'basic string validation',
13+
param: '',
14+
condition: (value: any) => typeof value === 'string' && value.trim().length > 0
15+
},
16+
{
17+
description: 'pattern in string validation',
18+
param: 'patternfly://lorem-ipsum',
19+
condition: (value: any) => new RegExp('patternfly://', 'i').test(value)
20+
},
21+
{
22+
description: 'array entry length validation',
23+
param: ['lorem', 'ipsum'],
24+
condition: (value: any) => Array.isArray(value) && value.length > 2
25+
}
26+
])('should throw an error for validation, $description', ({ param, condition }) => {
27+
const errorMessage = `Lorem ipsum error message for ${param} validation.`;
28+
29+
expect(() => assertInput(
30+
condition,
31+
errorMessage
32+
)).toThrow(errorMessage);
33+
});
34+
});
35+
36+
describe('assertInputString', () => {
37+
it.each([
38+
{
39+
description: 'empty string',
40+
param: ''
41+
},
42+
{
43+
description: 'undefined',
44+
param: undefined
45+
},
46+
{
47+
description: 'number',
48+
param: 1
49+
},
50+
{
51+
description: 'null',
52+
param: null
53+
}
54+
])('should throw an error for validation, $description', ({ param }) => {
55+
const errorMessage = '"Input" must be a string';
56+
57+
expect(() => assertInputString(
58+
param
59+
)).toThrow(errorMessage);
60+
});
61+
});
62+
63+
describe('assertInputStringLength', () => {
64+
it.each([
65+
{
66+
description: 'empty string',
67+
param: ''
68+
},
69+
{
70+
description: 'undefined',
71+
param: undefined
72+
},
73+
{
74+
description: 'number',
75+
param: 1
76+
},
77+
{
78+
description: 'null',
79+
param: null
80+
},
81+
{
82+
description: 'max',
83+
param: 'lorem ipsum',
84+
options: { max: 5 }
85+
},
86+
{
87+
description: 'min',
88+
param: 'lorem ipsum',
89+
options: { min: 15 }
90+
},
91+
{
92+
description: 'max and min',
93+
param: 'lorem ipsum',
94+
options: { min: 1, max: 10 }
95+
},
96+
{
97+
description: 'max and min and display name',
98+
param: 'lorem ipsum',
99+
options: { min: 1, max: 10, inputDisplayName: 'lorem ipsum' }
100+
},
101+
{
102+
description: 'max and min and description',
103+
param: 'lorem ipsum',
104+
options: { min: 1, max: 10, message: 'dolor sit amet, consectetur adipiscing elit.' }
105+
}
106+
])('should throw an error for validation, $description', ({ param, options }) => {
107+
const errorMessage = options?.message || `"${options?.inputDisplayName || 'Input'}" must be a string`;
108+
109+
expect(() => assertInputStringLength(
110+
param,
111+
{ min: 1, max: 100, ...options } as any
112+
)).toThrow(errorMessage);
113+
});
114+
});
115+
116+
describe('assertInputStringArrayEntryLength', () => {
117+
it.each([
118+
{
119+
description: 'empty string',
120+
param: ''
121+
},
122+
{
123+
description: 'undefined',
124+
param: undefined
125+
},
126+
{
127+
description: 'number',
128+
param: 1
129+
},
130+
{
131+
description: 'null',
132+
param: null
133+
},
134+
{
135+
description: 'max',
136+
param: ['lorem ipsum'],
137+
options: { max: 5 }
138+
},
139+
{
140+
description: 'min',
141+
param: ['lorem ipsum'],
142+
options: { min: 15 }
143+
},
144+
{
145+
description: 'max and min',
146+
param: ['lorem ipsum'],
147+
options: { min: 1, max: 10 }
148+
},
149+
{
150+
description: 'max and min and display name',
151+
param: ['lorem ipsum'],
152+
options: { min: 1, max: 10, inputDisplayName: 'lorem ipsum' }
153+
},
154+
{
155+
description: 'max and min and description',
156+
param: ['lorem ipsum'],
157+
options: { min: 1, max: 10, message: 'dolor sit amet, consectetur adipiscing elit.' }
158+
}
159+
])('should throw an error for validation, $description', ({ param, options }) => {
160+
const errorMessage = options?.message || `"${options?.inputDisplayName || 'Input'}" array must contain strings`;
161+
162+
expect(() => assertInputStringArrayEntryLength(
163+
param as any,
164+
{ min: 1, max: 100, ...options } as any
165+
)).toThrow(errorMessage);
166+
});
167+
});
168+
169+
describe('assertInputStringNumberEnumLike', () => {
170+
it.each([
171+
{
172+
description: 'empty string',
173+
param: '',
174+
compare: [2, 3]
175+
},
176+
{
177+
description: 'undefined',
178+
param: undefined,
179+
compare: [2, 3]
180+
},
181+
{
182+
description: 'null',
183+
param: null,
184+
compare: [2, 3]
185+
},
186+
{
187+
description: 'number',
188+
param: 1,
189+
compare: [2, 3]
190+
},
191+
{
192+
description: 'string',
193+
param: 'lorem ipsum',
194+
compare: ['amet', 'dolor sit']
195+
},
196+
{
197+
description: 'string and display name',
198+
param: 'lorem ipsum',
199+
compare: ['amet', 'dolor sit'],
200+
options: { inputDisplayName: 'lorem ipsum' }
201+
},
202+
{
203+
description: 'string and description',
204+
param: 'lorem ipsum',
205+
compare: [1, 2],
206+
options: { message: 'dolor sit amet, consectetur adipiscing elit.' }
207+
}
208+
])('should throw an error for validation, $description', ({ param, compare, options }) => {
209+
const errorMessage = options?.message || `"${options?.inputDisplayName || 'Input'}" must be one of the following values`;
210+
211+
expect(() => assertInputStringNumberEnumLike(
212+
param as any,
213+
compare as any,
214+
{ ...options } as any
215+
)).toThrow(errorMessage);
216+
});
217+
218+
it('should throw an internal error for validation when missing comparison values', () => {
219+
const errorMessage = 'List of allowed values is empty';
220+
221+
expect(() => assertInputStringNumberEnumLike(
222+
1,
223+
[]
224+
)).toThrow(errorMessage);
225+
});
226+
});

src/server.assertions.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import assert from 'node:assert';
2+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
3+
4+
/**
5+
* MCP assert. Centralizes and throws an error if the validation fails.
6+
*
7+
* @param condition - The function or condition to be validated.
8+
* @param message - The error message, or function that returns the error message, to be thrown if the validation fails.
9+
* @param {McpError} code - The error code to be thrown if the validation fails. Defaults to `ErrorCode.InvalidParams`.
10+
*
11+
* @throws {McpError} Throws the provided error message if the input validation fails.
12+
*/
13+
const mcpAssert = (condition: unknown, message: string | (() => string), code: ErrorCode = ErrorCode.InvalidParams) => {
14+
try {
15+
const result = typeof condition === 'function' ? condition() : condition;
16+
const resultMessage = typeof message === 'function' ? message() : message;
17+
18+
assert.ok(result, resultMessage);
19+
} catch (error) {
20+
throw new McpError(code, (error as Error).message);
21+
}
22+
};
23+
24+
/**
25+
* General purpose input assert/validation function.
26+
*
27+
* @alias mcpAssert
28+
*
29+
* @param condition - The function or condition to be validated.
30+
* @param message - The error message, or function that returns the error message, to be thrown if the validation fails.
31+
* @param {McpError} [code] - The error code to be thrown if the validation fails. Defaults to `ErrorCode.InvalidParams`.
32+
*
33+
* @throws {McpError} Throws the provided error message if the input validation fails.
34+
*/
35+
function assertInput(
36+
condition: unknown,
37+
message: string | (() => string),
38+
code?: ErrorCode
39+
): asserts condition {
40+
mcpAssert(condition, message, code);
41+
}
42+
43+
/**
44+
* Assert/validate if the input is a string.
45+
*
46+
* @param input - The input value
47+
* @param options - Validation options
48+
* @param options.inputDisplayName - Display name for the input. Used in the default error message. Defaults to 'Input'.
49+
* @param options.message - Custom error message. A default error message with optional `inputDisplayName` is generated if not provided.
50+
*
51+
* @throws McpError If input is not a string OR is empty
52+
*/
53+
function assertInputString(
54+
input: unknown,
55+
{ inputDisplayName, message }: { inputDisplayName?: string, message?: string } = {}
56+
): asserts input is string {
57+
const isValid = typeof input === 'string' && input.trim().length > 0;
58+
59+
mcpAssert(isValid, message || `"${inputDisplayName || 'Input'}" must be a string`);
60+
}
61+
62+
/**
63+
* Assert/validate if input is a string and meets length requirements.
64+
*
65+
* @param input - The input string
66+
* @param options - Validation options
67+
* @param options.max - Maximum length of the string
68+
* @param options.min - Minimum length of the string
69+
* @param options.inputDisplayName - Display name for the input. Used in the default error message. Defaults to 'Input'.
70+
* @param options.message - Error description. A default error message with optional `inputDisplayName` is generated if not provided.
71+
*
72+
* @throws McpError If input is not a string OR does not meet length requirements
73+
*/
74+
function assertInputStringLength(
75+
input: unknown,
76+
{ max, min, inputDisplayName, message }: { max: number, min: number, inputDisplayName?: string, message?: string }
77+
): asserts input is string {
78+
const isValid = typeof input === 'string' && input.length <= max && input.length >= min;
79+
80+
mcpAssert(isValid, message || `"${inputDisplayName || 'Input'}" must be a string from "${min}" to "${max}" characters`);
81+
}
82+
83+
/**
84+
* Assert/validate if input array entries are strings and have length.
85+
*
86+
* @param input - Array of strings
87+
* @param options - Validation options
88+
* @param options.max - Maximum length of each string in the array
89+
* @param options.min - Minimum length of each string in the array
90+
* @param options.inputDisplayName - Display name for the input. Used in the default error messages. Defaults to 'Input'.
91+
* @param options.message - Error description. A default error message with optional `inputDisplayName` is generated if not provided.
92+
*
93+
* @throws McpError If input is not an array of strings OR does not meet length requirements
94+
*/
95+
function assertInputStringArrayEntryLength(
96+
input: unknown[],
97+
{ max, min, inputDisplayName, message }: { max: number, min: number, inputDisplayName?: string, message?: string }
98+
): asserts input is string[] {
99+
const isValid = Array.isArray(input) && input.every(entry => typeof entry === 'string' && entry.length <= max && entry.length >= min);
100+
101+
mcpAssert(isValid, message || `"${inputDisplayName || 'Input'}" array must contain strings from "${min}" to "${max}" characters`);
102+
}
103+
104+
/**
105+
* Assert/validate if input is a string or number AND is one of the allowed values.
106+
*
107+
* @param input - The input value
108+
* @param values - List of allowed values
109+
* @param options - Validation options
110+
* @param options.inputDisplayName - Display name for the input. Used in the default error messages. Defaults to 'Input'.
111+
* @param options.message - Error description. A default error message with optional `inputDisplayName` is generated if not provided.
112+
*
113+
* @throws McpError If input is not a string or number OR is not one of the allowed values
114+
*/
115+
function assertInputStringNumberEnumLike(
116+
input: unknown,
117+
values: unknown[],
118+
{ inputDisplayName, message }: { inputDisplayName?: string, message?: string } = {}
119+
): asserts input is unknown[] {
120+
const hasArrayWithLength = Array.isArray(values) && values.length;
121+
let updatedDescription = message || `"${inputDisplayName || 'Input'}" must be one of the following values: ${values.join(', ')}`;
122+
let errorCode = ErrorCode.InvalidParams;
123+
124+
if (!hasArrayWithLength) {
125+
errorCode = ErrorCode.InternalError;
126+
updatedDescription = `Unable to confirm "${inputDisplayName || 'input'}." List of allowed values is empty or undefined.`;
127+
}
128+
129+
const isStringOrNumber = (typeof input === 'string' && typeof input.trim().length) || typeof input === 'number';
130+
const isValid = isStringOrNumber && hasArrayWithLength && values.includes(input);
131+
132+
mcpAssert(isValid, updatedDescription, errorCode);
133+
}
134+
135+
export {
136+
assertInput,
137+
assertInputString,
138+
assertInputStringLength,
139+
assertInputStringArrayEntryLength,
140+
assertInputStringNumberEnumLike
141+
};

0 commit comments

Comments
 (0)