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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/__tests__/helpers/test-server.d.ts +2 -0
- package/dist/__tests__/helpers/test-server.d.ts.map +1 -0
- package/dist/__tests__/helpers/test-server.js +27 -0
- package/dist/__tests__/helpers/test-server.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/mcp-http.d.ts +36 -0
- package/dist/clients/mcp-http.d.ts.map +1 -0
- package/dist/clients/mcp-http.js +148 -0
- package/dist/clients/mcp-http.js.map +1 -0
- package/dist/clients/mcp-stdio.d.ts +38 -0
- package/dist/clients/mcp-stdio.d.ts.map +1 -0
- package/dist/clients/mcp-stdio.js +164 -0
- package/dist/clients/mcp-stdio.js.map +1 -0
- package/dist/clients/types.d.ts +104 -0
- package/dist/clients/types.d.ts.map +1 -0
- package/dist/clients/types.js +8 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/core/prompt-registry.d.ts +56 -0
- package/dist/core/prompt-registry.d.ts.map +1 -0
- package/dist/core/prompt-registry.js +100 -0
- package/dist/core/prompt-registry.js.map +1 -0
- package/dist/core/resource-registry.d.ts +79 -0
- package/dist/core/resource-registry.d.ts.map +1 -0
- package/dist/core/resource-registry.js +135 -0
- package/dist/core/resource-registry.js.map +1 -0
- package/dist/core/resource-uri-templates.d.ts +64 -0
- package/dist/core/resource-uri-templates.d.ts.map +1 -0
- package/dist/core/resource-uri-templates.js +168 -0
- package/dist/core/resource-uri-templates.js.map +1 -0
- package/dist/core/server-http.d.ts +15 -0
- package/dist/core/server-http.d.ts.map +1 -0
- package/dist/core/server-http.js +302 -0
- package/dist/core/server-http.js.map +1 -0
- package/dist/core/server-stdio.d.ts +8 -0
- package/dist/core/server-stdio.d.ts.map +1 -0
- package/dist/core/server-stdio.js +15 -0
- package/dist/core/server-stdio.js.map +1 -0
- package/dist/core/server.d.ts +29 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +265 -0
- package/dist/core/server.js.map +1 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/dynamic-resource-manager.d.ts +115 -0
- package/dist/utils/dynamic-resource-manager.d.ts.map +1 -0
- package/dist/utils/dynamic-resource-manager.js +460 -0
- package/dist/utils/dynamic-resource-manager.js.map +1 -0
- package/dist/utils/http-client.d.ts +29 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +59 -0
- package/dist/utils/http-client.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/zod-to-mcp-schema.d.ts +42 -0
- package/dist/utils/zod-to-mcp-schema.d.ts.map +1 -0
- package/dist/utils/zod-to-mcp-schema.js +87 -0
- package/dist/utils/zod-to-mcp-schema.js.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/helpers/test-server.ts +31 -0
- package/src/__tests__/plugin-system.basic.test.ts +137 -0
- package/src/__tests__/server.basic.test.ts +37 -0
- package/src/__tests__/stdio-roundtrip.basic.test.ts +67 -0
- package/src/__tests__/tool-registry.basic.test.ts +114 -0
- package/src/__tests__/zod-schema.basic.test.ts +105 -0
- package/src/cli.ts +58 -0
- package/src/clients/mcp-http.ts +192 -0
- package/src/clients/mcp-stdio.ts +209 -0
- package/src/clients/types.ts +136 -0
- package/src/core/prompt-registry.ts +114 -0
- package/src/core/resource-registry.ts +166 -0
- package/src/core/resource-uri-templates.ts +216 -0
- package/src/core/server-http.ts +407 -0
- package/src/core/server-stdio.ts +20 -0
- package/src/core/server.ts +320 -0
- package/src/core/types.ts +312 -0
- package/src/index.ts +92 -0
- package/src/utils/dynamic-resource-manager.ts +581 -0
- package/src/utils/http-client.ts +86 -0
- package/src/utils/logger.ts +138 -0
- 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
|
+
}
|