@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/dist/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/dist/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
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mctx-ai/mcp-server",
3
+ "version": "0.3.0",
4
+ "description": "Build MCP servers with an Express-like API — no protocol knowledge required",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "types": "./src/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "anthropic",
22
+ "claude",
23
+ "server",
24
+ "framework"
25
+ ],
26
+ "author": "mctx, Inc.",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/mctx-ai/mcp-server.git",
31
+ "directory": "packages/server"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/mctx-ai/mcp-server/issues"
35
+ },
36
+ "homepage": "https://docs.mctx.ai",
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.19.0",
39
+ "@types/node": "^22.0.0",
40
+ "@vitest/coverage-v8": "^4.0.18",
41
+ "eslint": "^9.19.0",
42
+ "typescript": "^5.7.3",
43
+ "vitest": "^4.0.18"
44
+ },
45
+ "scripts": {
46
+ "build": "mkdir -p dist && cp src/*.js src/*.d.ts dist/",
47
+ "test": "vitest run",
48
+ "test:coverage": "vitest run --coverage",
49
+ "lint": "eslint src/ test/",
50
+ "lint:fix": "eslint src/ test/ --fix",
51
+ "typecheck": "tsc --noEmit"
52
+ }
53
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Completion Support Module
3
+ *
4
+ * Generates auto-completion suggestions for prompts, resources, and tool arguments.
5
+ * Supports custom completion handlers and auto-generation from schema metadata.
6
+ *
7
+ * @module completion
8
+ */
9
+
10
+ /**
11
+ * Maximum number of completion results to return
12
+ */
13
+ const MAX_COMPLETIONS = 100;
14
+
15
+ /**
16
+ * Generates completion suggestions for a reference
17
+ *
18
+ * Handles completion for:
19
+ * - Prompt arguments (ref/prompt-argument)
20
+ * - Resources (ref/resource)
21
+ *
22
+ * Supports:
23
+ * - Custom .complete() handler on registered items
24
+ * - Auto-generation from T.enum() values
25
+ * - Auto-generation from resource URI templates
26
+ *
27
+ * @param {Object} registeredItems - Map of registered prompts/resources
28
+ * @param {Object} ref - Reference object with type and name/uri
29
+ * @param {string} argumentValue - Partial text to complete against
30
+ * @returns {Object} Completion result: { completion: { values: [...], hasMore: boolean } }
31
+ *
32
+ * @example
33
+ * // Custom completion handler
34
+ * const items = {
35
+ * 'list-customers': {
36
+ * complete: async (argName, partialValue) => {
37
+ * if (argName === 'status') {
38
+ * return ['active', 'inactive', 'pending'];
39
+ * }
40
+ * return [];
41
+ * }
42
+ * }
43
+ * };
44
+ *
45
+ * generateCompletions(items, { type: 'ref/prompt-argument', name: 'list-customers' }, 'act')
46
+ * // => { completion: { values: ['active'], hasMore: false } }
47
+ *
48
+ * @example
49
+ * // Auto-generation from T.enum
50
+ * const items = {
51
+ * 'search': {
52
+ * input: {
53
+ * status: T.string({ enum: ['pending', 'active', 'completed'] })
54
+ * }
55
+ * }
56
+ * };
57
+ *
58
+ * generateCompletions(items, { type: 'ref/prompt-argument', name: 'search' }, 'p')
59
+ * // => { completion: { values: ['pending'], hasMore: false } }
60
+ */
61
+ export function generateCompletions(registeredItems, ref, argumentValue) {
62
+ // Validate inputs
63
+ if (!registeredItems || typeof registeredItems !== "object") {
64
+ return createEmptyCompletion();
65
+ }
66
+
67
+ if (!ref || !ref.type) {
68
+ return createEmptyCompletion();
69
+ }
70
+
71
+ const partialValue = argumentValue || "";
72
+
73
+ // Handle prompt argument completion
74
+ if (ref.type === "ref/prompt-argument") {
75
+ return generatePromptArgumentCompletions(
76
+ registeredItems,
77
+ ref,
78
+ partialValue,
79
+ );
80
+ }
81
+
82
+ // Handle resource completion
83
+ if (ref.type === "ref/resource") {
84
+ return generateResourceCompletions(registeredItems, ref, partialValue);
85
+ }
86
+
87
+ // Unknown reference type
88
+ return createEmptyCompletion();
89
+ }
90
+
91
+ /**
92
+ * Generate completions for prompt arguments
93
+ * @private
94
+ */
95
+ function generatePromptArgumentCompletions(registeredItems, ref, partialValue) {
96
+ const promptName = ref.name;
97
+ if (!promptName) return createEmptyCompletion();
98
+
99
+ const prompt = registeredItems[promptName];
100
+ if (!prompt) return createEmptyCompletion();
101
+
102
+ // Check for custom completion handler
103
+ if (typeof prompt.complete === "function") {
104
+ return executeCustomCompletion(
105
+ prompt.complete,
106
+ ref.argumentName,
107
+ partialValue,
108
+ );
109
+ }
110
+
111
+ // Auto-generate from T.enum values if available
112
+ if (prompt.input && ref.argumentName) {
113
+ const schema = prompt.input[ref.argumentName];
114
+ if (schema && schema.enum && Array.isArray(schema.enum)) {
115
+ return filterAndCap(schema.enum, partialValue);
116
+ }
117
+ }
118
+
119
+ return createEmptyCompletion();
120
+ }
121
+
122
+ /**
123
+ * Generate completions for resources
124
+ * @private
125
+ */
126
+ function generateResourceCompletions(registeredItems, ref, partialValue) {
127
+ const uri = ref.uri;
128
+ if (!uri) return createEmptyCompletion();
129
+
130
+ const resource = registeredItems[uri];
131
+ if (!resource) return createEmptyCompletion();
132
+
133
+ // Check for custom completion handler
134
+ if (typeof resource.complete === "function") {
135
+ return executeCustomCompletion(resource.complete, null, partialValue);
136
+ }
137
+
138
+ // Auto-generate from URI templates
139
+ // Extract possible values from template variables (limited - this is basic)
140
+ // In practice, custom handlers are recommended for dynamic resources
141
+
142
+ return createEmptyCompletion();
143
+ }
144
+
145
+ /**
146
+ * Execute custom completion handler
147
+ * @private
148
+ */
149
+ function executeCustomCompletion(completeFn, argumentName, partialValue) {
150
+ try {
151
+ const result = completeFn(argumentName, partialValue);
152
+
153
+ // Async completion handlers are not supported
154
+ if (result instanceof Promise) {
155
+ throw new Error(
156
+ "Async completion handlers are not supported. Use synchronous handlers for fn.complete.",
157
+ );
158
+ }
159
+
160
+ // Filter and cap the results
161
+ if (Array.isArray(result)) {
162
+ return filterAndCap(result, partialValue);
163
+ }
164
+
165
+ return createEmptyCompletion();
166
+ } catch (error) {
167
+ console.error("Completion handler error:", error);
168
+ return createEmptyCompletion();
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Filter completion values by partial match and cap at MAX_COMPLETIONS
174
+ * @private
175
+ * @param {Array<string>} values - All possible values
176
+ * @param {string} partialValue - User's partial input
177
+ * @returns {Object} Completion result
178
+ */
179
+ function filterAndCap(values, partialValue) {
180
+ if (!Array.isArray(values)) {
181
+ return createEmptyCompletion();
182
+ }
183
+
184
+ // Filter values that start with partial input (case-insensitive)
185
+ const lowerPartial = partialValue.toLowerCase();
186
+ const filtered = values.filter((value) => {
187
+ if (typeof value !== "string") return false;
188
+ return value.toLowerCase().startsWith(lowerPartial);
189
+ });
190
+
191
+ // Cap at MAX_COMPLETIONS
192
+ const hasMore = filtered.length > MAX_COMPLETIONS;
193
+ const capped = filtered.slice(0, MAX_COMPLETIONS);
194
+
195
+ return {
196
+ completion: {
197
+ values: capped,
198
+ hasMore,
199
+ },
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Create empty completion result
205
+ * @private
206
+ */
207
+ function createEmptyCompletion() {
208
+ return {
209
+ completion: {
210
+ values: [],
211
+ hasMore: false,
212
+ },
213
+ };
214
+ }