@mctx-ai/mcp-server 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * T - JSON Schema type system for MCP tool and prompt inputs
3
+ *
4
+ * Provides factory methods to build JSON Schema objects with a clean API.
5
+ * Supports type validation, constraints, and nested schemas.
6
+ */
7
+
8
+ /**
9
+ * Creates a string type schema
10
+ * @param {Object} options - Schema options
11
+ * @param {boolean} [options.required] - Mark field as required (metadata for buildInputSchema)
12
+ * @param {string} [options.description] - Field description
13
+ * @param {Array} [options.enum] - Allowed values
14
+ * @param {*} [options.default] - Default value
15
+ * @param {number} [options.minLength] - Minimum string length
16
+ * @param {number} [options.maxLength] - Maximum string length
17
+ * @param {string} [options.pattern] - Regex pattern
18
+ * @param {string} [options.format] - Format (email, uri, date-time, etc.)
19
+ * @returns {Object} JSON Schema object
20
+ */
21
+ function string(options = {}) {
22
+ const schema = { type: "string" };
23
+
24
+ if (options.description !== undefined)
25
+ schema.description = options.description;
26
+ if (options.enum !== undefined) schema.enum = options.enum;
27
+ if (options.default !== undefined) schema.default = options.default;
28
+ if (options.minLength !== undefined) schema.minLength = options.minLength;
29
+ if (options.maxLength !== undefined) schema.maxLength = options.maxLength;
30
+ if (options.pattern !== undefined) schema.pattern = options.pattern;
31
+ if (options.format !== undefined) schema.format = options.format;
32
+
33
+ // Store required as metadata (not a JSON Schema keyword on properties)
34
+ if (options.required === true) schema._required = true;
35
+
36
+ return schema;
37
+ }
38
+
39
+ /**
40
+ * Creates a number type schema
41
+ * @param {Object} options - Schema options
42
+ * @param {boolean} [options.required] - Mark field as required (metadata for buildInputSchema)
43
+ * @param {string} [options.description] - Field description
44
+ * @param {Array} [options.enum] - Allowed values
45
+ * @param {*} [options.default] - Default value
46
+ * @param {number} [options.min] - Minimum value (maps to 'minimum')
47
+ * @param {number} [options.max] - Maximum value (maps to 'maximum')
48
+ * @returns {Object} JSON Schema object
49
+ */
50
+ function number(options = {}) {
51
+ const schema = { type: "number" };
52
+
53
+ if (options.description !== undefined)
54
+ schema.description = options.description;
55
+ if (options.enum !== undefined) schema.enum = options.enum;
56
+ if (options.default !== undefined) schema.default = options.default;
57
+ if (options.min !== undefined) schema.minimum = options.min;
58
+ if (options.max !== undefined) schema.maximum = options.max;
59
+
60
+ if (options.required === true) schema._required = true;
61
+
62
+ return schema;
63
+ }
64
+
65
+ /**
66
+ * Creates a boolean type schema
67
+ * @param {Object} options - Schema options
68
+ * @param {boolean} [options.required] - Mark field as required (metadata for buildInputSchema)
69
+ * @param {string} [options.description] - Field description
70
+ * @param {*} [options.default] - Default value
71
+ * @returns {Object} JSON Schema object
72
+ */
73
+ function boolean(options = {}) {
74
+ const schema = { type: "boolean" };
75
+
76
+ if (options.description !== undefined)
77
+ schema.description = options.description;
78
+ if (options.default !== undefined) schema.default = options.default;
79
+
80
+ if (options.required === true) schema._required = true;
81
+
82
+ return schema;
83
+ }
84
+
85
+ /**
86
+ * Creates an array type schema
87
+ * @param {Object} options - Schema options
88
+ * @param {boolean} [options.required] - Mark field as required (metadata for buildInputSchema)
89
+ * @param {string} [options.description] - Field description
90
+ * @param {Object} [options.items] - Schema for array items
91
+ * @param {*} [options.default] - Default value
92
+ * @returns {Object} JSON Schema object
93
+ */
94
+ function array(options = {}) {
95
+ const schema = { type: "array" };
96
+
97
+ if (options.description !== undefined)
98
+ schema.description = options.description;
99
+ if (options.default !== undefined) schema.default = options.default;
100
+ if (options.items !== undefined) {
101
+ // Clean _required metadata from items schema
102
+ schema.items = cleanMetadata(options.items);
103
+ }
104
+
105
+ if (options.required === true) schema._required = true;
106
+
107
+ return schema;
108
+ }
109
+
110
+ /**
111
+ * Creates an object type schema
112
+ * @param {Object} options - Schema options
113
+ * @param {boolean} [options.required] - Mark field as required (metadata for buildInputSchema)
114
+ * @param {string} [options.description] - Field description
115
+ * @param {Object} [options.properties] - Nested property schemas
116
+ * @param {boolean|Object} [options.additionalProperties] - Allow additional properties
117
+ * @param {*} [options.default] - Default value
118
+ * @returns {Object} JSON Schema object
119
+ */
120
+ function object(options = {}) {
121
+ const schema = { type: "object" };
122
+
123
+ if (options.description !== undefined)
124
+ schema.description = options.description;
125
+ if (options.default !== undefined) schema.default = options.default;
126
+
127
+ // Handle nested properties
128
+ if (options.properties !== undefined) {
129
+ const { properties, required } = buildProperties(options.properties);
130
+ schema.properties = properties;
131
+ if (required.length > 0) schema.required = required;
132
+ }
133
+
134
+ if (options.additionalProperties !== undefined) {
135
+ if (typeof options.additionalProperties === "boolean") {
136
+ schema.additionalProperties = options.additionalProperties;
137
+ } else {
138
+ // Clean metadata from additionalProperties schema
139
+ schema.additionalProperties = cleanMetadata(options.additionalProperties);
140
+ }
141
+ }
142
+
143
+ if (options.required === true) schema._required = true;
144
+
145
+ return schema;
146
+ }
147
+
148
+ /**
149
+ * Builds properties object and extracts required fields
150
+ * Helper for buildInputSchema and object()
151
+ * @param {Object} properties - Properties map
152
+ * @returns {{properties: Object, required: Array<string>}} Cleaned properties and required array
153
+ */
154
+ function buildProperties(properties) {
155
+ if (!properties || typeof properties !== "object") {
156
+ return { properties: {}, required: [] };
157
+ }
158
+
159
+ const cleanedProperties = {};
160
+ const required = [];
161
+
162
+ for (const [key, schema] of Object.entries(properties)) {
163
+ if (!schema || typeof schema !== "object") continue;
164
+
165
+ // Extract required metadata
166
+ if (schema._required === true) {
167
+ required.push(key);
168
+ }
169
+
170
+ // Clean the schema (remove metadata)
171
+ cleanedProperties[key] = cleanMetadata(schema);
172
+ }
173
+
174
+ return { properties: cleanedProperties, required };
175
+ }
176
+
177
+ /**
178
+ * Removes framework metadata from schema
179
+ * @param {Object} schema - Schema object
180
+ * @returns {Object} Cleaned schema
181
+ */
182
+ function cleanMetadata(schema) {
183
+ if (!schema || typeof schema !== "object") return schema;
184
+
185
+ // Create shallow copy
186
+ const cleaned = { ...schema };
187
+
188
+ // Remove metadata
189
+ delete cleaned._required;
190
+
191
+ // Recursively clean nested schemas
192
+ if (cleaned.properties && typeof cleaned.properties === "object") {
193
+ const { properties, required } = buildProperties(cleaned.properties);
194
+ cleaned.properties = properties;
195
+ if (required.length > 0) cleaned.required = required;
196
+ }
197
+
198
+ if (cleaned.items && typeof cleaned.items === "object") {
199
+ cleaned.items = cleanMetadata(cleaned.items);
200
+ }
201
+
202
+ if (
203
+ cleaned.additionalProperties &&
204
+ typeof cleaned.additionalProperties === "object"
205
+ ) {
206
+ cleaned.additionalProperties = cleanMetadata(cleaned.additionalProperties);
207
+ }
208
+
209
+ return cleaned;
210
+ }
211
+
212
+ /**
213
+ * Builds a complete MCP input schema from handler input definition
214
+ * @param {Object} input - Handler input definition using T types
215
+ * @returns {Object} Valid JSON Schema for MCP inputSchema
216
+ */
217
+ export function buildInputSchema(input) {
218
+ // Handle null/undefined input
219
+ if (!input) {
220
+ return {
221
+ type: "object",
222
+ properties: {},
223
+ };
224
+ }
225
+
226
+ // Build properties and extract required fields
227
+ const { properties, required } = buildProperties(input);
228
+
229
+ const schema = {
230
+ type: "object",
231
+ properties,
232
+ };
233
+
234
+ // Only add required array if not empty
235
+ if (required.length > 0) {
236
+ schema.required = required;
237
+ }
238
+
239
+ return schema;
240
+ }
241
+
242
+ /**
243
+ * T - Type factory object
244
+ * Exports all type builders
245
+ */
246
+ export const T = {
247
+ string,
248
+ number,
249
+ boolean,
250
+ array,
251
+ object,
252
+ };
package/src/uri.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * URI Template Matching Module
3
+ *
4
+ * Implements RFC 6570 Level 1 URI templates (simple string expansion {var}).
5
+ * Provides utilities for matching request URIs against registered resource URIs
6
+ * and extracting template parameters.
7
+ *
8
+ * @module uri
9
+ */
10
+
11
+ /**
12
+ * Checks if a URI contains template variables
13
+ * @param {string} uri - The URI to check
14
+ * @returns {boolean} True if URI contains {param} syntax
15
+ *
16
+ * @example
17
+ * isTemplate('db://customers/123') // false
18
+ * isTemplate('db://customers/{id}') // true
19
+ * isTemplate('db://products/{id}/reviews') // true
20
+ */
21
+ export function isTemplate(uri) {
22
+ if (!uri || typeof uri !== "string") return false;
23
+ return /\{[^}]+\}/.test(uri);
24
+ }
25
+
26
+ /**
27
+ * Extracts template variable names from a URI
28
+ * @param {string} uri - The URI template
29
+ * @returns {string[]} Array of variable names
30
+ * @throws {Error} If template variable name is invalid
31
+ *
32
+ * @example
33
+ * extractTemplateVars('db://customers/{id}') // ['id']
34
+ * extractTemplateVars('db://products/{category}/items/{id}') // ['category', 'id']
35
+ * extractTemplateVars('db://customers/123') // []
36
+ */
37
+ export function extractTemplateVars(uri) {
38
+ if (!uri || typeof uri !== "string") return [];
39
+
40
+ const matches = uri.matchAll(/\{([^}]+)\}/g);
41
+ const vars = [];
42
+
43
+ for (const match of matches) {
44
+ const varName = match[1];
45
+
46
+ // Validate: alphanumeric + underscore only (RFC 6570 Level 1)
47
+ if (!/^[a-zA-Z0-9_]+$/.test(varName)) {
48
+ throw new Error(
49
+ `Invalid template variable name: "${varName}". Must contain only alphanumeric characters and underscores.`,
50
+ );
51
+ }
52
+
53
+ vars.push(varName);
54
+ }
55
+
56
+ return vars;
57
+ }
58
+
59
+ /**
60
+ * Matches a request URI against a registered URI template
61
+ *
62
+ * Returns null for no match, or an object with extracted parameters for match.
63
+ * Handles both static URIs (exact match) and dynamic URIs (template expansion).
64
+ *
65
+ * @param {string} registeredUri - The registered URI (may contain {param} templates)
66
+ * @param {string} requestUri - The incoming request URI (no templates)
67
+ * @returns {Object|null} Match result: { params: { name: value, ... } } or null
68
+ *
69
+ * @example
70
+ * // Static URI - exact match
71
+ * matchUri('db://customers/schema', 'db://customers/schema')
72
+ * // => { params: {} }
73
+ *
74
+ * matchUri('db://customers/schema', 'db://customers/list')
75
+ * // => null
76
+ *
77
+ * // Dynamic URI - parameter extraction
78
+ * matchUri('db://customers/{id}', 'db://customers/123')
79
+ * // => { params: { id: '123' } }
80
+ *
81
+ * matchUri('db://products/{category}/items/{id}', 'db://products/electronics/items/456')
82
+ * // => { params: { category: 'electronics', id: '456' } }
83
+ *
84
+ * matchUri('db://customers/{id}', 'db://products/123')
85
+ * // => null (different path)
86
+ */
87
+ export function matchUri(registeredUri, requestUri) {
88
+ // Guard clauses
89
+ if (!registeredUri || typeof registeredUri !== "string") return null;
90
+ if (!requestUri || typeof requestUri !== "string") return null;
91
+
92
+ // Static route: exact string match
93
+ if (!isTemplate(registeredUri)) {
94
+ return registeredUri === requestUri ? { params: {} } : null;
95
+ }
96
+
97
+ // Dynamic route: build regex pattern and extract params
98
+ const templateVars = extractTemplateVars(registeredUri);
99
+
100
+ // Escape regex special characters except {placeholders}
101
+ // Convert {var} to named capture group
102
+ let pattern = registeredUri
103
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Escape regex chars
104
+ .replace(/\\\{([^}]+)\\\}/g, "([^/]+)"); // Convert {var} to capture group
105
+
106
+ pattern = `^${pattern}$`; // Exact match (anchored)
107
+
108
+ const regex = new RegExp(pattern);
109
+ const match = requestUri.match(regex);
110
+
111
+ if (!match) return null;
112
+
113
+ // Extract parameters from capture groups
114
+ const params = {};
115
+ for (let i = 0; i < templateVars.length; i++) {
116
+ params[templateVars[i]] = match[i + 1]; // match[0] is full string, params start at [1]
117
+ }
118
+
119
+ return { params };
120
+ }