mcp-orbit 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/dist/__tests__/helpers/test-server.d.ts +2 -0
  4. package/dist/__tests__/helpers/test-server.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/test-server.js +27 -0
  6. package/dist/__tests__/helpers/test-server.js.map +1 -0
  7. package/dist/cli.d.ts +23 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +56 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/clients/mcp-http.d.ts +36 -0
  12. package/dist/clients/mcp-http.d.ts.map +1 -0
  13. package/dist/clients/mcp-http.js +148 -0
  14. package/dist/clients/mcp-http.js.map +1 -0
  15. package/dist/clients/mcp-stdio.d.ts +38 -0
  16. package/dist/clients/mcp-stdio.d.ts.map +1 -0
  17. package/dist/clients/mcp-stdio.js +164 -0
  18. package/dist/clients/mcp-stdio.js.map +1 -0
  19. package/dist/clients/types.d.ts +104 -0
  20. package/dist/clients/types.d.ts.map +1 -0
  21. package/dist/clients/types.js +8 -0
  22. package/dist/clients/types.js.map +1 -0
  23. package/dist/core/prompt-registry.d.ts +56 -0
  24. package/dist/core/prompt-registry.d.ts.map +1 -0
  25. package/dist/core/prompt-registry.js +100 -0
  26. package/dist/core/prompt-registry.js.map +1 -0
  27. package/dist/core/resource-registry.d.ts +79 -0
  28. package/dist/core/resource-registry.d.ts.map +1 -0
  29. package/dist/core/resource-registry.js +135 -0
  30. package/dist/core/resource-registry.js.map +1 -0
  31. package/dist/core/resource-uri-templates.d.ts +64 -0
  32. package/dist/core/resource-uri-templates.d.ts.map +1 -0
  33. package/dist/core/resource-uri-templates.js +168 -0
  34. package/dist/core/resource-uri-templates.js.map +1 -0
  35. package/dist/core/server-http.d.ts +15 -0
  36. package/dist/core/server-http.d.ts.map +1 -0
  37. package/dist/core/server-http.js +302 -0
  38. package/dist/core/server-http.js.map +1 -0
  39. package/dist/core/server-stdio.d.ts +8 -0
  40. package/dist/core/server-stdio.d.ts.map +1 -0
  41. package/dist/core/server-stdio.js +15 -0
  42. package/dist/core/server-stdio.js.map +1 -0
  43. package/dist/core/server.d.ts +29 -0
  44. package/dist/core/server.d.ts.map +1 -0
  45. package/dist/core/server.js +265 -0
  46. package/dist/core/server.js.map +1 -0
  47. package/dist/core/types.d.ts +265 -0
  48. package/dist/core/types.d.ts.map +1 -0
  49. package/dist/core/types.js +9 -0
  50. package/dist/core/types.js.map +1 -0
  51. package/dist/index.d.ts +28 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +26 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/utils/dynamic-resource-manager.d.ts +115 -0
  56. package/dist/utils/dynamic-resource-manager.d.ts.map +1 -0
  57. package/dist/utils/dynamic-resource-manager.js +460 -0
  58. package/dist/utils/dynamic-resource-manager.js.map +1 -0
  59. package/dist/utils/http-client.d.ts +29 -0
  60. package/dist/utils/http-client.d.ts.map +1 -0
  61. package/dist/utils/http-client.js +59 -0
  62. package/dist/utils/http-client.js.map +1 -0
  63. package/dist/utils/logger.d.ts +25 -0
  64. package/dist/utils/logger.d.ts.map +1 -0
  65. package/dist/utils/logger.js +105 -0
  66. package/dist/utils/logger.js.map +1 -0
  67. package/dist/utils/zod-to-mcp-schema.d.ts +42 -0
  68. package/dist/utils/zod-to-mcp-schema.d.ts.map +1 -0
  69. package/dist/utils/zod-to-mcp-schema.js +87 -0
  70. package/dist/utils/zod-to-mcp-schema.js.map +1 -0
  71. package/package.json +57 -0
  72. package/src/__tests__/helpers/test-server.ts +31 -0
  73. package/src/__tests__/plugin-system.basic.test.ts +137 -0
  74. package/src/__tests__/server.basic.test.ts +37 -0
  75. package/src/__tests__/stdio-roundtrip.basic.test.ts +67 -0
  76. package/src/__tests__/tool-registry.basic.test.ts +114 -0
  77. package/src/__tests__/zod-schema.basic.test.ts +105 -0
  78. package/src/cli.ts +58 -0
  79. package/src/clients/mcp-http.ts +192 -0
  80. package/src/clients/mcp-stdio.ts +209 -0
  81. package/src/clients/types.ts +136 -0
  82. package/src/core/prompt-registry.ts +114 -0
  83. package/src/core/resource-registry.ts +166 -0
  84. package/src/core/resource-uri-templates.ts +216 -0
  85. package/src/core/server-http.ts +407 -0
  86. package/src/core/server-stdio.ts +20 -0
  87. package/src/core/server.ts +320 -0
  88. package/src/core/types.ts +312 -0
  89. package/src/index.ts +92 -0
  90. package/src/utils/dynamic-resource-manager.ts +581 -0
  91. package/src/utils/http-client.ts +86 -0
  92. package/src/utils/logger.ts +138 -0
  93. package/src/utils/zod-to-mcp-schema.ts +127 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * MCP Client Types & Interfaces
3
+ *
4
+ * Shared types for mcp-orbit clients
5
+ * Used by both real clients and mock clients
6
+ */
7
+
8
+ // ============================================================================
9
+ // SERVER CONFIGURATION
10
+ // ============================================================================
11
+
12
+ export interface McpStdioServerConfig {
13
+ command: string;
14
+ args?: string[];
15
+ env?: Record<string, string>;
16
+ cwd?: string;
17
+ }
18
+
19
+ export interface McpHttpServerConfig {
20
+ url: string;
21
+ headers?: Record<string, string>;
22
+ timeout?: number;
23
+ }
24
+
25
+ export type McpServerConfig = McpStdioServerConfig | McpHttpServerConfig;
26
+
27
+ // ============================================================================
28
+ // TOOLS
29
+ // ============================================================================
30
+
31
+ export interface MCPToolSchema {
32
+ name: string;
33
+ description?: string;
34
+ input_schema: {
35
+ type: "object";
36
+ properties?: Record<string, any>;
37
+ required?: string[];
38
+ [key: string]: any;
39
+ };
40
+ tags?: string[]; // Classification tags (filtered by the workflow, not forwarded to the LLM)
41
+ }
42
+
43
+ export interface MCPToolResponse {
44
+ content: MCPContent[];
45
+ isError?: boolean;
46
+ structuredContent?: unknown;
47
+ _meta?: {
48
+ resourceUri?: string;
49
+ cached?: boolean;
50
+ size?: number;
51
+ cacheExpiry?: number;
52
+ toolName?: string;
53
+ };
54
+ }
55
+
56
+ export interface MCPContent {
57
+ type: "text" | "image" | "resource";
58
+ text?: string;
59
+ data?: string;
60
+ mimeType?: string;
61
+ }
62
+
63
+ // ============================================================================
64
+ // RESOURCES
65
+ // ============================================================================
66
+
67
+ export interface MCPResource {
68
+ uri: string;
69
+ name: string;
70
+ description?: string;
71
+ title?: string;
72
+ mimeType?: string;
73
+ toolset?: string;
74
+ scope?: string;
75
+ }
76
+
77
+ export interface MCPResourceList {
78
+ resources: MCPResource[];
79
+ }
80
+
81
+ export interface MCPResourceContent {
82
+ contents: Array<{
83
+ uri: string;
84
+ text?: string;
85
+ blob?: string;
86
+ }>;
87
+ }
88
+
89
+ // ============================================================================
90
+ // PROMPTS
91
+ // ============================================================================
92
+
93
+ export interface MCPPromptArgument {
94
+ name: string;
95
+ description?: string;
96
+ required?: boolean;
97
+ type?: "string" | "number" | "boolean" | "array" | "object";
98
+ }
99
+
100
+ export interface MCPPrompt {
101
+ name: string;
102
+ description?: string;
103
+ arguments?: MCPPromptArgument[];
104
+ }
105
+
106
+ export interface MCPPromptList {
107
+ prompts: MCPPrompt[];
108
+ }
109
+
110
+ export interface MCPPromptMessage {
111
+ role: "user" | "assistant";
112
+ content: {
113
+ type: "text" | "image" | "resource";
114
+ text?: string;
115
+ data?: string;
116
+ mimeType?: string;
117
+ };
118
+ }
119
+
120
+ export interface MCPPromptResponse {
121
+ messages: MCPPromptMessage[];
122
+ }
123
+
124
+ // ============================================================================
125
+ // CLIENT INTERFACE
126
+ // ============================================================================
127
+
128
+ export interface IMcpClient {
129
+ listTools(timeoutMs?: number): Promise<{tools: MCPToolSchema[]}>;
130
+ callTool(toolName: string, args: Record<string, any>, timeoutMs?: number): Promise<MCPToolResponse>;
131
+ listResources(timeoutMs?: number): Promise<MCPResourceList>;
132
+ readResource(uri: string, timeoutMs?: number): Promise<MCPResourceContent>;
133
+ listPrompts(timeoutMs?: number): Promise<MCPPromptList>;
134
+ getPrompt(name: string, args?: Record<string, any>, timeoutMs?: number): Promise<MCPPromptResponse>;
135
+ close(): Promise<void>;
136
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Prompt Registry - Simplified
3
+ *
4
+ * Auto-registration pattern with direct array access.
5
+ *
6
+ * Prompts = Templated messages & workflows for users
7
+ * - Chat templates with variables
8
+ * - Workflow assistants
9
+ * - Guided interactions
10
+ */
11
+
12
+ import type {MCPPromptDefinition, PromptProvider, PromptMessage} from "./types.js";
13
+ import logger from "../utils/logger.js";
14
+
15
+ const promptLogger = logger.child("prompts");
16
+
17
+ // Global prompts array - single source of truth
18
+ const prompts: PromptProvider[] = [];
19
+
20
+ /**
21
+ * Create and register a prompt provider
22
+ *
23
+ * Usage:
24
+ * const prompt = createAndRegisterPrompt({ name, description, render: async () => ({ messages }) });
25
+ *
26
+ * This function is called during prompt module imports (auto-registration pattern)
27
+ */
28
+ export function createAndRegisterPrompt(prompt: PromptProvider): PromptProvider {
29
+ if (prompts.some((p) => p.name === prompt.name)) {
30
+ promptLogger.warn(`Prompt '${prompt.name}' already registered, overwriting...`);
31
+ }
32
+
33
+ prompts.push(prompt);
34
+ promptLogger.info(`Prompt registered: ${prompt.name}`);
35
+ return prompt;
36
+ }
37
+
38
+ /**
39
+ * Get all prompt definitions (for MCP ListPrompts response)
40
+ */
41
+ export function getPromptDefinitions(): MCPPromptDefinition[] {
42
+ return prompts.map((provider) => ({
43
+ name: provider.name,
44
+ description: provider.description,
45
+ arguments: provider.arguments,
46
+ toolset: provider.toolset,
47
+ tags: provider.tags,
48
+ category: provider.category,
49
+ usage: provider.usage,
50
+ maxTokens: provider.maxTokens,
51
+ }));
52
+ }
53
+
54
+ /**
55
+ * Get a prompt by name
56
+ */
57
+ export function getPrompt(name: string): PromptProvider | undefined {
58
+ return prompts.find((p) => p.name === name);
59
+ }
60
+
61
+ /**
62
+ * Get a prompt with rendered messages (prompts/get)
63
+ *
64
+ * Handles:
65
+ * - Prompt lookup
66
+ * - Arguments validation via Zod schema
67
+ * - Template rendering
68
+ */
69
+ export async function renderPrompt(name: string, args?: Record<string, any>): Promise<{messages: PromptMessage[]}> {
70
+ const prompt = getPrompt(name);
71
+
72
+ if (!prompt) {
73
+ throw new Error(`Prompt not found: ${name}`);
74
+ }
75
+
76
+ // Validate arguments if schema is provided
77
+ if (prompt.argumentsSchema && args) {
78
+ try {
79
+ prompt.argumentsSchema.parse(args);
80
+ } catch (error) {
81
+ throw new Error(`Invalid prompt arguments: ${String(error)}`);
82
+ }
83
+ }
84
+
85
+ // Execute prompt provider (render template)
86
+ const messages = await prompt.render(args);
87
+
88
+ return {messages};
89
+ }
90
+
91
+ /**
92
+ * Get count of registered prompts
93
+ */
94
+ export function getPromptCount(): number {
95
+ return prompts.length;
96
+ }
97
+
98
+ /**
99
+ * Check if a prompt exists
100
+ */
101
+ export function hasPrompt(name: string): boolean {
102
+ return prompts.some((p) => p.name === name);
103
+ }
104
+
105
+ /** Singleton registry object — convenience wrapper over the named exports. */
106
+ export const promptRegistry = {
107
+ register: createAndRegisterPrompt,
108
+ listDefinitions: getPromptDefinitions,
109
+ get: renderPrompt,
110
+ get count(): number {
111
+ return getPromptCount();
112
+ },
113
+ has: hasPrompt,
114
+ };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Resource Registry - Simplified with RFC 6570 Templates
3
+ *
4
+ * Auto-registration pattern with:
5
+ * - Direct array access for static resources
6
+ * - RFC 6570 URI template support for dynamic resources
7
+ */
8
+
9
+ import type {MCPResource, ResourceContent, ResourceProvider} from "./types.js";
10
+ import {TemplateRegistry} from "./resource-uri-templates.js";
11
+ import logger from "../utils/logger.js";
12
+
13
+ const resourceLogger = logger.child("resources");
14
+
15
+ type RegistryContentBlock = {
16
+ uri: string;
17
+ name?: string;
18
+ title?: string;
19
+ mimeType?: string;
20
+ text?: string;
21
+ blob?: string;
22
+ annotations?: MCPResource["annotations"];
23
+ size?: number;
24
+ schema?: MCPResource["schema"];
25
+ };
26
+
27
+ // Global resources array - single source of truth
28
+ const resources: ResourceProvider[] = [];
29
+
30
+ // Template registry for RFC 6570 URI template support
31
+ const templateRegistry = new TemplateRegistry();
32
+
33
+ /**
34
+ * Create and register a resource provider
35
+ *
36
+ * Usage:
37
+ * const resource = createAndRegisterResource({ uri, name, read: async () => ({ text }) });
38
+ *
39
+ * Supports both static URIs and RFC 6570 URI templates:
40
+ * - Static: "cache://users/123"
41
+ * - Template: "cache://users/{id}"
42
+ *
43
+ * This function is called during resource module imports (auto-registration pattern)
44
+ */
45
+ export function createAndRegisterResource(resource: ResourceProvider): ResourceProvider {
46
+ if (resources.some((r) => r.uri === resource.uri)) {
47
+ resourceLogger.warn(`Resource '${resource.uri}' already registered, overwriting...`);
48
+ }
49
+
50
+ resources.push(resource);
51
+
52
+ if (resource.uri.includes("{")) {
53
+ templateRegistry.registerTemplate(resource.uri);
54
+ resourceLogger.info(`Resource registered (template): ${resource.uri}`);
55
+ } else {
56
+ resourceLogger.info(`Resource registered: ${resource.uri}`);
57
+ }
58
+
59
+ return resource;
60
+ }
61
+
62
+ /**
63
+ * Unregister a resource by URI (used when TTL expires)
64
+ */
65
+ export function unregisterResource(uri: string): void {
66
+ const index = resources.findIndex((r) => r.uri === uri);
67
+ if (index !== -1) {
68
+ resources.splice(index, 1);
69
+ resourceLogger.info(`Resource unregistered: ${uri}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get all resource definitions (for MCP ListResources response)
75
+ */
76
+ export function getResourceDefinitions(): MCPResource[] {
77
+ return resources.map((provider) => ({
78
+ uri: provider.uri,
79
+ name: provider.name,
80
+ description: provider.description,
81
+ title: provider.title,
82
+ mimeType: provider.mimeType,
83
+ size: provider.size,
84
+ schema: provider.schema,
85
+ annotations: provider.annotations,
86
+ toolset: provider.toolset,
87
+ scope: provider.scope,
88
+ capabilities: provider.capabilities,
89
+ }));
90
+ }
91
+
92
+ /**
93
+ * Get a resource by URI
94
+ */
95
+ export function getResource(uri: string): ResourceProvider | undefined {
96
+ return resources.find((r) => r.uri === uri);
97
+ }
98
+
99
+ /**
100
+ * Read a resource (resources/read)
101
+ */
102
+ export async function readResource(uri: string): Promise<{contents: RegistryContentBlock[]}> {
103
+ const resource = getResource(uri);
104
+
105
+ if (!resource) {
106
+ throw new Error(`Resource not found: ${uri}`);
107
+ }
108
+
109
+ const content: ResourceContent = await resource.read();
110
+
111
+ return {
112
+ contents: [
113
+ {
114
+ uri: resource.uri,
115
+ name: resource.name,
116
+ title: resource.title,
117
+ mimeType: resource.mimeType,
118
+ size: resource.size,
119
+ schema: resource.schema,
120
+ annotations: resource.annotations,
121
+ ...content,
122
+ },
123
+ ],
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Get count of registered resources
129
+ */
130
+ export function getResourceCount(): number {
131
+ return resources.length;
132
+ }
133
+
134
+ /**
135
+ * Check if a resource exists
136
+ */
137
+ export function hasResource(uri: string): boolean {
138
+ return resources.some((r) => r.uri === uri);
139
+ }
140
+
141
+ /**
142
+ * Get all registered RFC 6570 URI template patterns
143
+ */
144
+ export function getTemplatePatterns(): string[] {
145
+ return templateRegistry.getTemplates();
146
+ }
147
+
148
+ /**
149
+ * Get template registry (for advanced template operations)
150
+ */
151
+ export function getTemplateRegistry(): TemplateRegistry {
152
+ return templateRegistry;
153
+ }
154
+
155
+ export const resourceRegistry = {
156
+ register: createAndRegisterResource,
157
+ unregister: unregisterResource,
158
+ listDefinitions: getResourceDefinitions,
159
+ read: readResource,
160
+ get count(): number {
161
+ return getResourceCount();
162
+ },
163
+ has: hasResource,
164
+ getTemplates: getTemplatePatterns,
165
+ getTemplateRegistry,
166
+ };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * RFC 6570 URI Template Support
3
+ *
4
+ * Implements RFC 6570 URI template matching for resource patterns.
5
+ * Enables pattern-based resource definitions like:
6
+ * - cache://tools/{id}
7
+ * - kraken://balance/{pair}
8
+ *
9
+ * See: https://tools.ietf.org/html/rfc6570
10
+ */
11
+
12
+ /**
13
+ * RFC 6570 template variables with operators:
14
+ * - Simple expansion: {var}
15
+ * - Reserved expansion: {+var}
16
+ * - Fragment expansion: {#var}
17
+ * - Label expansion: {.var}
18
+ * - Path expansion: {/var}
19
+ * - Parameter expansion: {?var}
20
+ * - Query continuation: {&var}
21
+ */
22
+
23
+ export interface URITemplate {
24
+ pattern: string;
25
+ regex: RegExp;
26
+ variables: string[];
27
+ }
28
+
29
+ export interface URIMatch {
30
+ matched: boolean;
31
+ variables: Record<string, string>;
32
+ }
33
+
34
+ /**
35
+ * Parse RFC 6570 URI template and create a regex matcher
36
+ */
37
+ export function parseURITemplate(template: string): URITemplate {
38
+ // Extract all template variables: {var}, {+var}, {#var}, etc.
39
+ const variableRegex = /\{([+#./?&]?)([a-zA-Z_][a-zA-Z0-9_]*(?:,[a-zA-Z_][a-zA-Z0-9_]*)*)\}/g;
40
+ const variables: string[] = [];
41
+ const parts: Array<{type: "literal" | "pattern"; value: string}> = [];
42
+ let lastIndex = 0;
43
+
44
+ // Build regex pattern from template
45
+ let match: RegExpExecArray | null;
46
+ while ((match = variableRegex.exec(template)) !== null) {
47
+ // Add literal part before this match
48
+ if (match.index > lastIndex) {
49
+ const literal = template.substring(lastIndex, match.index);
50
+ parts.push({
51
+ type: "literal",
52
+ value: literal.replace(/[.+?^${}()[\]|\\]/g, "\\$&"), // Escape special regex chars
53
+ });
54
+ }
55
+
56
+ const operator = match[1]; // e.g., '+', '#', '.', '/', '?', '&'
57
+ const varNames = match[2].split(","); // Handle multiple variables in one template
58
+
59
+ varNames.forEach((varName) => {
60
+ if (!variables.includes(varName)) {
61
+ variables.push(varName);
62
+ }
63
+ });
64
+
65
+ // Add regex pattern for this template variable
66
+ const replacement = getRegexForOperator(operator, varNames);
67
+ parts.push({
68
+ type: "pattern",
69
+ value: replacement,
70
+ });
71
+
72
+ lastIndex = match.index + match[0].length;
73
+ }
74
+
75
+ // Add any remaining literal part
76
+ if (lastIndex < template.length) {
77
+ const literal = template.substring(lastIndex);
78
+ parts.push({
79
+ type: "literal",
80
+ value: literal.replace(/[.+?^${}()[\]|\\]/g, "\\$&"),
81
+ });
82
+ }
83
+
84
+ const regexPattern = parts.map((p) => p.value).join("");
85
+ const regex = new RegExp(`^${regexPattern}$`);
86
+
87
+ return {
88
+ pattern: template,
89
+ regex,
90
+ variables,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Get regex pattern for RFC 6570 operator
96
+ */
97
+ function getRegexForOperator(operator: string, _varNames: string[]): string {
98
+ // Different operators allow different characters
99
+ switch (operator) {
100
+ case "+": // Reserved characters allowed
101
+ case "#": // Fragment (reserved chars allowed)
102
+ return `([a-zA-Z0-9\\-_.~:/?#[\\]@!$&'()*+,;=]*)`;
103
+
104
+ case ".": // Label (dots, no slashes)
105
+ return `([a-zA-Z0-9\\-_~]*)`;
106
+
107
+ case "/": // Path (no leading slash in capture)
108
+ return `([a-zA-Z0-9\\-_~./]*)`;
109
+
110
+ case "?": // Query (unreserved + reserved)
111
+ case "&": // Query continuation
112
+ return `([a-zA-Z0-9\\-_.~:/?#[\\]@!$&'()*+,;=%]*)`;
113
+
114
+ default: // Simple expansion (unreserved chars only: A-Z a-z 0-9 - . _ ~)
115
+ return `([a-zA-Z0-9\\-_~.]*)`;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Match a URI against a template and extract variables
121
+ */
122
+ export function matchURITemplate(uri: string, template: URITemplate): URIMatch {
123
+ const regexMatch = template.regex.exec(uri);
124
+
125
+ if (!regexMatch) {
126
+ return {matched: false, variables: {}};
127
+ }
128
+
129
+ const variables: Record<string, string> = {};
130
+ template.variables.forEach((varName, index) => {
131
+ variables[varName] = regexMatch[index + 1] || "";
132
+ });
133
+
134
+ return {matched: true, variables};
135
+ }
136
+
137
+ /**
138
+ * Resolve a URI template with specific variable values
139
+ * Example: resolveURITemplate("cache://tools/{id}", { id: "123" }) => "cache://tools/123"
140
+ */
141
+ export function resolveURITemplate(template: string, variables: Record<string, string>): string {
142
+ let resolved = template;
143
+ const variableRegex = /\{([+#./?&]?)([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
144
+
145
+ let match: RegExpExecArray | null;
146
+ while ((match = variableRegex.exec(template)) !== null) {
147
+ const operator = match[1];
148
+ const varName = match[2];
149
+ const value = variables[varName] || "";
150
+
151
+ // Build the replacement based on operator
152
+ let replacement = "";
153
+ switch (operator) {
154
+ case "+":
155
+ replacement = value; // Reserved chars allowed, no prefix
156
+ break;
157
+ case "#":
158
+ replacement = value ? `#${value}` : "";
159
+ break;
160
+ case ".":
161
+ replacement = value ? `.${value}` : "";
162
+ break;
163
+ case "/":
164
+ replacement = value ? `/${value}` : "";
165
+ break;
166
+ case "?":
167
+ replacement = value ? `?${varName}=${value}` : "";
168
+ break;
169
+ case "&":
170
+ replacement = value ? `&${varName}=${value}` : "";
171
+ break;
172
+ default:
173
+ replacement = value; // Simple expansion
174
+ }
175
+
176
+ resolved = resolved.replace(match[0], replacement);
177
+ }
178
+
179
+ return resolved;
180
+ }
181
+
182
+ /**
183
+ * Template Registry for matching dynamic resources
184
+ */
185
+ export class TemplateRegistry {
186
+ private templates: Map<string, URITemplate> = new Map();
187
+
188
+ /**
189
+ * Register a URI template pattern
190
+ */
191
+ registerTemplate(pattern: string): void {
192
+ if (!this.templates.has(pattern)) {
193
+ this.templates.set(pattern, parseURITemplate(pattern));
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Find matching template for a given URI
199
+ */
200
+ findMatchingTemplate(uri: string): {pattern: string; match: URIMatch} | null {
201
+ for (const [pattern, template] of this.templates) {
202
+ const match = matchURITemplate(uri, template);
203
+ if (match.matched) {
204
+ return {pattern, match};
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Get all registered templates
212
+ */
213
+ getTemplates(): string[] {
214
+ return Array.from(this.templates.keys());
215
+ }
216
+ }