@mctx-ai/mcp-server 0.5.0 → 0.5.2

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/index.d.ts CHANGED
@@ -187,7 +187,10 @@ export type ToolHandler = {
187
187
  * ```
188
188
  */
189
189
  export type GeneratorToolHandler = {
190
- (args: Record<string, any>, ask?: AskFunction | null): Generator<any, any, any> | AsyncGenerator<any, any, any>;
190
+ (
191
+ args: Record<string, any>,
192
+ ask?: AskFunction | null,
193
+ ): Generator<any, any, any> | AsyncGenerator<any, any, any>;
191
194
  description?: string;
192
195
  input?: Record<string, SchemaDefinition>;
193
196
  mimeType?: string;
@@ -202,7 +205,10 @@ export type GeneratorToolHandler = {
202
205
  * @returns Resource content
203
206
  */
204
207
  export type ResourceHandler = {
205
- (params: Record<string, string>, ask?: AskFunction | null): any | Promise<any>;
208
+ (
209
+ params: Record<string, string>,
210
+ ask?: AskFunction | null,
211
+ ): any | Promise<any>;
206
212
  /** Resource name for display */
207
213
  name?: string;
208
214
  /** Resource description */
@@ -220,7 +226,14 @@ export type ResourceHandler = {
220
226
  * @returns Prompt messages (string, conversation result, or message array)
221
227
  */
222
228
  export type PromptHandler = {
223
- (args: Record<string, any>, ask?: AskFunction | null): string | ConversationResult | Message[] | Promise<string | ConversationResult | Message[]>;
229
+ (
230
+ args: Record<string, any>,
231
+ ask?: AskFunction | null,
232
+ ):
233
+ | string
234
+ | ConversationResult
235
+ | Message[]
236
+ | Promise<string | ConversationResult | Message[]>;
224
237
  /** Prompt description */
225
238
  description?: string;
226
239
  /** Input schema definition using T types */
@@ -291,7 +304,7 @@ export interface SamplingOptions {
291
304
  * Represents a JSON Schema property with optional metadata.
292
305
  */
293
306
  export interface SchemaDefinition {
294
- type: 'string' | 'number' | 'boolean' | 'array' | 'object';
307
+ type: "string" | "number" | "boolean" | "array" | "object";
295
308
  description?: string;
296
309
  enum?: any[];
297
310
  default?: any;
@@ -477,7 +490,7 @@ export const T: {
477
490
  * ```
478
491
  */
479
492
  export function buildInputSchema(input?: Record<string, SchemaDefinition>): {
480
- type: 'object';
493
+ type: "object";
481
494
  properties: Record<string, SchemaDefinition>;
482
495
  required?: string[];
483
496
  };
@@ -491,7 +504,7 @@ export function buildInputSchema(input?: Record<string, SchemaDefinition>): {
491
504
  */
492
505
  export interface Message {
493
506
  /** Message role */
494
- role: 'user' | 'assistant';
507
+ role: "user" | "assistant";
495
508
  /** Message content */
496
509
  content: TextContent | ImageContent | ResourceContent;
497
510
  }
@@ -500,7 +513,7 @@ export interface Message {
500
513
  * Text content type.
501
514
  */
502
515
  export interface TextContent {
503
- type: 'text';
516
+ type: "text";
504
517
  text: string;
505
518
  }
506
519
 
@@ -508,7 +521,7 @@ export interface TextContent {
508
521
  * Image content type (base64-encoded).
509
522
  */
510
523
  export interface ImageContent {
511
- type: 'image';
524
+ type: "image";
512
525
  data: string;
513
526
  mimeType: string;
514
527
  }
@@ -517,7 +530,7 @@ export interface ImageContent {
517
530
  * Resource content type (embedded resource).
518
531
  */
519
532
  export interface ResourceContent {
520
- type: 'resource';
533
+ type: "resource";
521
534
  resource: {
522
535
  uri: string;
523
536
  text: string;
@@ -588,7 +601,7 @@ export interface ConversationHelpers {
588
601
  * ```
589
602
  */
590
603
  export function conversation(
591
- builderFn: (helpers: ConversationHelpers) => Message[]
604
+ builderFn: (helpers: ConversationHelpers) => Message[],
592
605
  ): ConversationResult;
593
606
 
594
607
  // ============================================================================
@@ -600,7 +613,7 @@ export function conversation(
600
613
  * Yielded by generator tools to report progress.
601
614
  */
602
615
  export interface ProgressNotification {
603
- type: 'progress';
616
+ type: "progress";
604
617
  /** Current step number (1-indexed) */
605
618
  progress: number;
606
619
  /** Total steps (optional, for determinate progress) */
@@ -666,7 +679,15 @@ export const PROGRESS_DEFAULTS: {
666
679
  /**
667
680
  * Log severity level (RFC 5424).
668
681
  */
669
- export type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
682
+ export type LogLevel =
683
+ | "debug"
684
+ | "info"
685
+ | "notice"
686
+ | "warning"
687
+ | "error"
688
+ | "critical"
689
+ | "alert"
690
+ | "emergency";
670
691
 
671
692
  /**
672
693
  * Log object with methods for each severity level.
package/dist/security.js CHANGED
@@ -187,18 +187,18 @@ export function validateStringInput(value, maxLength = 10485760) {
187
187
  }
188
188
 
189
189
  /**
190
- * Validate URI scheme against allowlist
190
+ * Validate URI scheme against denylist
191
191
  *
192
192
  * Security: Prevents injection attacks via dangerous URI schemes
193
- * - Blocks file://, javascript:, data:, etc. by default
194
- * - Allows http:// and https:// by default
195
- * - Supports custom allowlists for special cases
193
+ * - Blocks file://, javascript:, data:, vbscript:, about: by default
194
+ * - Allows all other schemes (http://, https://, custom schemes like docs://)
195
+ * - MCP spec explicitly supports custom URI schemes for resources
196
+ * - Validates scheme format per RFC 3986
196
197
  *
197
198
  * @param {string} uri - URI to validate
198
- * @param {string[]} allowedSchemes - Allowed schemes (default: ['http', 'https'])
199
- * @returns {boolean} True if URI scheme is allowed
199
+ * @returns {boolean} True if URI scheme is safe (not in denylist)
200
200
  */
201
- export function validateUriScheme(uri, allowedSchemes = ["http", "https"]) {
201
+ export function validateUriScheme(uri) {
202
202
  if (!uri || typeof uri !== "string") return false;
203
203
 
204
204
  // Extract scheme (characters before first colon)
@@ -207,13 +207,25 @@ export function validateUriScheme(uri, allowedSchemes = ["http", "https"]) {
207
207
 
208
208
  const scheme = schemeMatch[1].toLowerCase();
209
209
 
210
+ // RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
211
+ // Must start with a letter, followed by letters, digits, +, -, or .
212
+ const schemeRegex = /^[a-z][a-z0-9+.-]*$/;
213
+ if (!schemeRegex.test(scheme)) {
214
+ return false;
215
+ }
216
+
217
+ // Reasonable length limit to prevent abuse
218
+ if (scheme.length > 32) {
219
+ return false;
220
+ }
221
+
210
222
  // Check if scheme is explicitly dangerous
211
223
  if (DANGEROUS_SCHEMES.includes(scheme)) {
212
224
  return false;
213
225
  }
214
226
 
215
- // Check if scheme is in allowlist
216
- return allowedSchemes.some((allowed) => allowed.toLowerCase() === scheme);
227
+ // All other schemes (including custom ones) are allowed
228
+ return true;
217
229
  }
218
230
 
219
231
  /**
package/dist/server.js CHANGED
@@ -438,24 +438,31 @@ export function createServer(options = {}) {
438
438
  // Validate URI scheme (prevent dangerous schemes like file://, javascript:, data:)
439
439
  if (!validateUriScheme(uri)) {
440
440
  throw new Error(
441
- `Invalid URI scheme: only http:// and https:// are allowed`,
441
+ `Disallowed URI scheme: dangerous schemes like file://, javascript:, data: are not allowed`,
442
442
  );
443
443
  }
444
444
 
445
- // Canonicalize path to prevent traversal attacks
446
- const canonicalUri = canonicalizePath(uri);
445
+ // Apply canonicalizePath to ALL URIs for consistent security validation
446
+ // This catches: null bytes, Unicode variants, multi-layer encoding, mixed encoding
447
+ // Path normalization (slash deduplication, backslash conversion) is safe for all schemes
448
+ const normalizedUri = canonicalizePath(uri);
447
449
 
448
450
  // Find matching resource (try exact match first, then templates)
449
451
  let handler = null;
450
452
  let extractedParams = {};
451
453
 
452
- // Try exact match first (use canonical URI for matching)
453
- if (resources.has(canonicalUri)) {
454
- handler = resources.get(canonicalUri);
454
+ // Try exact match first (use normalized URI for matching)
455
+ if (resources.has(normalizedUri)) {
456
+ handler = resources.get(normalizedUri);
455
457
  } else {
456
458
  // Try template matching using uri.js module
457
459
  for (const [registeredUri, h] of resources.entries()) {
458
- const match = matchUri(registeredUri, canonicalUri);
460
+ // Normalize registered URI for consistent matching (slash deduplication only)
461
+ // Don't apply security validation to developer-provided templates
462
+ const normalizedRegisteredUri = registeredUri
463
+ .replace(/\\/g, "/") // Convert backslashes to forward slashes
464
+ .replace(/\/+/g, "/"); // Remove duplicate slashes
465
+ const match = matchUri(normalizedRegisteredUri, normalizedUri);
459
466
  if (match) {
460
467
  handler = h;
461
468
  extractedParams = match.params || {};
@@ -783,11 +790,9 @@ export function createServer(options = {}) {
783
790
  return handleLoggingSetLevel(params);
784
791
 
785
792
  default: {
786
- {
787
- const error = new Error("Method not found");
788
- error.code = -32601;
789
- throw error;
790
- }
793
+ const error = new Error("Method not found");
794
+ error.code = -32601;
795
+ throw error;
791
796
  }
792
797
  }
793
798
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mctx-ai/mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Build MCP servers with an Express-like API — no protocol knowledge required",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.d.ts CHANGED
@@ -187,7 +187,10 @@ export type ToolHandler = {
187
187
  * ```
188
188
  */
189
189
  export type GeneratorToolHandler = {
190
- (args: Record<string, any>, ask?: AskFunction | null): Generator<any, any, any> | AsyncGenerator<any, any, any>;
190
+ (
191
+ args: Record<string, any>,
192
+ ask?: AskFunction | null,
193
+ ): Generator<any, any, any> | AsyncGenerator<any, any, any>;
191
194
  description?: string;
192
195
  input?: Record<string, SchemaDefinition>;
193
196
  mimeType?: string;
@@ -202,7 +205,10 @@ export type GeneratorToolHandler = {
202
205
  * @returns Resource content
203
206
  */
204
207
  export type ResourceHandler = {
205
- (params: Record<string, string>, ask?: AskFunction | null): any | Promise<any>;
208
+ (
209
+ params: Record<string, string>,
210
+ ask?: AskFunction | null,
211
+ ): any | Promise<any>;
206
212
  /** Resource name for display */
207
213
  name?: string;
208
214
  /** Resource description */
@@ -220,7 +226,14 @@ export type ResourceHandler = {
220
226
  * @returns Prompt messages (string, conversation result, or message array)
221
227
  */
222
228
  export type PromptHandler = {
223
- (args: Record<string, any>, ask?: AskFunction | null): string | ConversationResult | Message[] | Promise<string | ConversationResult | Message[]>;
229
+ (
230
+ args: Record<string, any>,
231
+ ask?: AskFunction | null,
232
+ ):
233
+ | string
234
+ | ConversationResult
235
+ | Message[]
236
+ | Promise<string | ConversationResult | Message[]>;
224
237
  /** Prompt description */
225
238
  description?: string;
226
239
  /** Input schema definition using T types */
@@ -291,7 +304,7 @@ export interface SamplingOptions {
291
304
  * Represents a JSON Schema property with optional metadata.
292
305
  */
293
306
  export interface SchemaDefinition {
294
- type: 'string' | 'number' | 'boolean' | 'array' | 'object';
307
+ type: "string" | "number" | "boolean" | "array" | "object";
295
308
  description?: string;
296
309
  enum?: any[];
297
310
  default?: any;
@@ -477,7 +490,7 @@ export const T: {
477
490
  * ```
478
491
  */
479
492
  export function buildInputSchema(input?: Record<string, SchemaDefinition>): {
480
- type: 'object';
493
+ type: "object";
481
494
  properties: Record<string, SchemaDefinition>;
482
495
  required?: string[];
483
496
  };
@@ -491,7 +504,7 @@ export function buildInputSchema(input?: Record<string, SchemaDefinition>): {
491
504
  */
492
505
  export interface Message {
493
506
  /** Message role */
494
- role: 'user' | 'assistant';
507
+ role: "user" | "assistant";
495
508
  /** Message content */
496
509
  content: TextContent | ImageContent | ResourceContent;
497
510
  }
@@ -500,7 +513,7 @@ export interface Message {
500
513
  * Text content type.
501
514
  */
502
515
  export interface TextContent {
503
- type: 'text';
516
+ type: "text";
504
517
  text: string;
505
518
  }
506
519
 
@@ -508,7 +521,7 @@ export interface TextContent {
508
521
  * Image content type (base64-encoded).
509
522
  */
510
523
  export interface ImageContent {
511
- type: 'image';
524
+ type: "image";
512
525
  data: string;
513
526
  mimeType: string;
514
527
  }
@@ -517,7 +530,7 @@ export interface ImageContent {
517
530
  * Resource content type (embedded resource).
518
531
  */
519
532
  export interface ResourceContent {
520
- type: 'resource';
533
+ type: "resource";
521
534
  resource: {
522
535
  uri: string;
523
536
  text: string;
@@ -588,7 +601,7 @@ export interface ConversationHelpers {
588
601
  * ```
589
602
  */
590
603
  export function conversation(
591
- builderFn: (helpers: ConversationHelpers) => Message[]
604
+ builderFn: (helpers: ConversationHelpers) => Message[],
592
605
  ): ConversationResult;
593
606
 
594
607
  // ============================================================================
@@ -600,7 +613,7 @@ export function conversation(
600
613
  * Yielded by generator tools to report progress.
601
614
  */
602
615
  export interface ProgressNotification {
603
- type: 'progress';
616
+ type: "progress";
604
617
  /** Current step number (1-indexed) */
605
618
  progress: number;
606
619
  /** Total steps (optional, for determinate progress) */
@@ -666,7 +679,15 @@ export const PROGRESS_DEFAULTS: {
666
679
  /**
667
680
  * Log severity level (RFC 5424).
668
681
  */
669
- export type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
682
+ export type LogLevel =
683
+ | "debug"
684
+ | "info"
685
+ | "notice"
686
+ | "warning"
687
+ | "error"
688
+ | "critical"
689
+ | "alert"
690
+ | "emergency";
670
691
 
671
692
  /**
672
693
  * Log object with methods for each severity level.
package/src/security.js CHANGED
@@ -187,18 +187,18 @@ export function validateStringInput(value, maxLength = 10485760) {
187
187
  }
188
188
 
189
189
  /**
190
- * Validate URI scheme against allowlist
190
+ * Validate URI scheme against denylist
191
191
  *
192
192
  * Security: Prevents injection attacks via dangerous URI schemes
193
- * - Blocks file://, javascript:, data:, etc. by default
194
- * - Allows http:// and https:// by default
195
- * - Supports custom allowlists for special cases
193
+ * - Blocks file://, javascript:, data:, vbscript:, about: by default
194
+ * - Allows all other schemes (http://, https://, custom schemes like docs://)
195
+ * - MCP spec explicitly supports custom URI schemes for resources
196
+ * - Validates scheme format per RFC 3986
196
197
  *
197
198
  * @param {string} uri - URI to validate
198
- * @param {string[]} allowedSchemes - Allowed schemes (default: ['http', 'https'])
199
- * @returns {boolean} True if URI scheme is allowed
199
+ * @returns {boolean} True if URI scheme is safe (not in denylist)
200
200
  */
201
- export function validateUriScheme(uri, allowedSchemes = ["http", "https"]) {
201
+ export function validateUriScheme(uri) {
202
202
  if (!uri || typeof uri !== "string") return false;
203
203
 
204
204
  // Extract scheme (characters before first colon)
@@ -207,13 +207,25 @@ export function validateUriScheme(uri, allowedSchemes = ["http", "https"]) {
207
207
 
208
208
  const scheme = schemeMatch[1].toLowerCase();
209
209
 
210
+ // RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
211
+ // Must start with a letter, followed by letters, digits, +, -, or .
212
+ const schemeRegex = /^[a-z][a-z0-9+.-]*$/;
213
+ if (!schemeRegex.test(scheme)) {
214
+ return false;
215
+ }
216
+
217
+ // Reasonable length limit to prevent abuse
218
+ if (scheme.length > 32) {
219
+ return false;
220
+ }
221
+
210
222
  // Check if scheme is explicitly dangerous
211
223
  if (DANGEROUS_SCHEMES.includes(scheme)) {
212
224
  return false;
213
225
  }
214
226
 
215
- // Check if scheme is in allowlist
216
- return allowedSchemes.some((allowed) => allowed.toLowerCase() === scheme);
227
+ // All other schemes (including custom ones) are allowed
228
+ return true;
217
229
  }
218
230
 
219
231
  /**
package/src/server.js CHANGED
@@ -438,24 +438,31 @@ export function createServer(options = {}) {
438
438
  // Validate URI scheme (prevent dangerous schemes like file://, javascript:, data:)
439
439
  if (!validateUriScheme(uri)) {
440
440
  throw new Error(
441
- `Invalid URI scheme: only http:// and https:// are allowed`,
441
+ `Disallowed URI scheme: dangerous schemes like file://, javascript:, data: are not allowed`,
442
442
  );
443
443
  }
444
444
 
445
- // Canonicalize path to prevent traversal attacks
446
- const canonicalUri = canonicalizePath(uri);
445
+ // Apply canonicalizePath to ALL URIs for consistent security validation
446
+ // This catches: null bytes, Unicode variants, multi-layer encoding, mixed encoding
447
+ // Path normalization (slash deduplication, backslash conversion) is safe for all schemes
448
+ const normalizedUri = canonicalizePath(uri);
447
449
 
448
450
  // Find matching resource (try exact match first, then templates)
449
451
  let handler = null;
450
452
  let extractedParams = {};
451
453
 
452
- // Try exact match first (use canonical URI for matching)
453
- if (resources.has(canonicalUri)) {
454
- handler = resources.get(canonicalUri);
454
+ // Try exact match first (use normalized URI for matching)
455
+ if (resources.has(normalizedUri)) {
456
+ handler = resources.get(normalizedUri);
455
457
  } else {
456
458
  // Try template matching using uri.js module
457
459
  for (const [registeredUri, h] of resources.entries()) {
458
- const match = matchUri(registeredUri, canonicalUri);
460
+ // Normalize registered URI for consistent matching (slash deduplication only)
461
+ // Don't apply security validation to developer-provided templates
462
+ const normalizedRegisteredUri = registeredUri
463
+ .replace(/\\/g, "/") // Convert backslashes to forward slashes
464
+ .replace(/\/+/g, "/"); // Remove duplicate slashes
465
+ const match = matchUri(normalizedRegisteredUri, normalizedUri);
459
466
  if (match) {
460
467
  handler = h;
461
468
  extractedParams = match.params || {};
@@ -783,11 +790,9 @@ export function createServer(options = {}) {
783
790
  return handleLoggingSetLevel(params);
784
791
 
785
792
  default: {
786
- {
787
- const error = new Error("Method not found");
788
- error.code = -32601;
789
- throw error;
790
- }
793
+ const error = new Error("Method not found");
794
+ error.code = -32601;
795
+ throw error;
791
796
  }
792
797
  }
793
798
  }