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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic HTTP Client Utility
|
|
3
|
+
*
|
|
4
|
+
* Pure HTTP logic without MCP-specific types.
|
|
5
|
+
* Used by providers (kraken-api, etc.) for making HTTP requests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {request} from "undici";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// HTTP CLIENT CONFIG
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface HTTPClientConfig {
|
|
15
|
+
url: string;
|
|
16
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
body?: string;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
maxResponseSize?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HTTPClientResponse {
|
|
24
|
+
statusCode: number;
|
|
25
|
+
headers: Record<string, string>;
|
|
26
|
+
body: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// HTTP CLIENT
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export class HTTPClient {
|
|
34
|
+
private defaultTimeout = 30000; // 30 seconds
|
|
35
|
+
private defaultMaxSize = 10 * 1024 * 1024; // 10 MB
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute HTTP request with timeout and size limits
|
|
39
|
+
*/
|
|
40
|
+
async request(config: HTTPClientConfig): Promise<HTTPClientResponse> {
|
|
41
|
+
const timeout = config.timeout || this.defaultTimeout;
|
|
42
|
+
const maxSize = config.maxResponseSize || this.defaultMaxSize;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Execute request with timeout
|
|
46
|
+
const response = await request(config.url, {
|
|
47
|
+
method: config.method,
|
|
48
|
+
headers: config.headers,
|
|
49
|
+
body: config.body,
|
|
50
|
+
signal: AbortSignal.timeout(timeout),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Read body with size limit
|
|
54
|
+
const chunks: Buffer[] = [];
|
|
55
|
+
let totalSize = 0;
|
|
56
|
+
|
|
57
|
+
for await (const chunk of response.body) {
|
|
58
|
+
totalSize += chunk.length;
|
|
59
|
+
if (totalSize > maxSize) {
|
|
60
|
+
throw new Error(`Response size exceeded ${maxSize} bytes`);
|
|
61
|
+
}
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Convert headers to Record<string, string>
|
|
66
|
+
const headers: Record<string, string> = {};
|
|
67
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
68
|
+
headers[key] = Array.isArray(value) ? value[0] : value || "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
statusCode: response.statusCode,
|
|
73
|
+
headers,
|
|
74
|
+
body: Buffer.concat(chunks).toString("utf-8"),
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`HTTP request failed: ${String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Singleton instance
|
|
86
|
+
export const httpClient = new HTTPClient();
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {inspect} from "node:util";
|
|
3
|
+
|
|
4
|
+
export type LogLevel = "debug" | "info" | "success" | "warn" | "error";
|
|
5
|
+
|
|
6
|
+
export interface Logger {
|
|
7
|
+
debug: (...args: unknown[]) => void;
|
|
8
|
+
info: (...args: unknown[]) => void;
|
|
9
|
+
success: (...args: unknown[]) => void;
|
|
10
|
+
warn: (...args: unknown[]) => void;
|
|
11
|
+
error: (...args: unknown[]) => void;
|
|
12
|
+
child: (scope: string, levelOverride?: LogLevel) => Logger;
|
|
13
|
+
level: LogLevel;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
17
|
+
debug: 10,
|
|
18
|
+
info: 20,
|
|
19
|
+
success: 25,
|
|
20
|
+
warn: 30,
|
|
21
|
+
error: 40,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const LEVEL_COLORS: Record<LogLevel, typeof chalk.gray> = {
|
|
25
|
+
debug: chalk.gray,
|
|
26
|
+
info: chalk.blue,
|
|
27
|
+
success: chalk.green,
|
|
28
|
+
warn: chalk.yellow,
|
|
29
|
+
error: chalk.red,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const LEVEL_WRITERS: Record<LogLevel, (...args: unknown[]) => void> = {
|
|
33
|
+
debug: (...args) => getWriter("debug")(...args),
|
|
34
|
+
info: (...args) => getWriter("info")(...args),
|
|
35
|
+
success: (...args) => getWriter("success")(...args),
|
|
36
|
+
warn: (...args) => getWriter("warn")(...args),
|
|
37
|
+
error: (...args) => getWriter("error")(...args),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const GLOBAL_LEVEL = resolveLogLevel(process.env.MCP_LOG_LEVEL ?? process.env.LOG_LEVEL ?? "info");
|
|
41
|
+
|
|
42
|
+
export let forceStdErrMode = false;
|
|
43
|
+
export function _setForceStdErrMode(value: boolean) {
|
|
44
|
+
forceStdErrMode = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getWriter(level: LogLevel) {
|
|
48
|
+
if (forceStdErrMode) {
|
|
49
|
+
return console.error;
|
|
50
|
+
}
|
|
51
|
+
switch (level) {
|
|
52
|
+
case "debug":
|
|
53
|
+
case "info":
|
|
54
|
+
case "success":
|
|
55
|
+
return console.log;
|
|
56
|
+
case "warn":
|
|
57
|
+
return console.warn;
|
|
58
|
+
case "error":
|
|
59
|
+
return console.error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Public log function for quick one-off logs without creating a scoped logger.
|
|
65
|
+
*/
|
|
66
|
+
export function log(level: LogLevel, ...args: unknown[]) {
|
|
67
|
+
writeLog(level, GLOBAL_LEVEL, undefined, args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a scoped logger instance. Child loggers inherit the current level unless overridden.
|
|
72
|
+
*/
|
|
73
|
+
export function createLogger(scope?: string, level: LogLevel = GLOBAL_LEVEL): Logger {
|
|
74
|
+
const write =
|
|
75
|
+
(targetLevel: LogLevel) =>
|
|
76
|
+
(...args: unknown[]) =>
|
|
77
|
+
writeLog(targetLevel, level, scope, args);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
level,
|
|
81
|
+
debug: write("debug"),
|
|
82
|
+
info: write("info"),
|
|
83
|
+
success: write("success"),
|
|
84
|
+
warn: write("warn"),
|
|
85
|
+
error: write("error"),
|
|
86
|
+
child(childScope: string, levelOverride?: LogLevel) {
|
|
87
|
+
const nextScope = scope ? `${scope}:${childScope}` : childScope;
|
|
88
|
+
return createLogger(nextScope, levelOverride ?? level);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const defaultLogger = createLogger();
|
|
94
|
+
|
|
95
|
+
export default defaultLogger;
|
|
96
|
+
export {LOG_LEVELS};
|
|
97
|
+
|
|
98
|
+
function writeLog(level: LogLevel, threshold: LogLevel, scope: string | undefined, args: unknown[]) {
|
|
99
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[threshold]) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefix = LEVEL_COLORS[level](`[${level.toUpperCase()}]`);
|
|
104
|
+
const scopeLabel = scope ? chalk.magenta(`[${scope}]`) : undefined;
|
|
105
|
+
const timestamp = chalk.dim(new Date().toISOString());
|
|
106
|
+
const writer = LEVEL_WRITERS[level];
|
|
107
|
+
const formatter = LEVEL_COLORS[level];
|
|
108
|
+
const formattedArgs = args.map((value) => formatter(formatValue(value)));
|
|
109
|
+
|
|
110
|
+
const header = [timestamp, prefix, scopeLabel].filter(Boolean);
|
|
111
|
+
writer(...header, ...formattedArgs);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveLogLevel(raw: string): LogLevel {
|
|
115
|
+
const normalized = raw.toLowerCase() as LogLevel;
|
|
116
|
+
if (normalized in LOG_LEVELS) {
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "info";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatValue(value: unknown): string {
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (value instanceof Error) {
|
|
129
|
+
return value.stack ?? `${value.name}: ${value.message}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return inspect(value, {
|
|
133
|
+
depth: 5,
|
|
134
|
+
breakLength: 80,
|
|
135
|
+
compact: false,
|
|
136
|
+
colors: chalk.level > 0,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { zodToJsonSchema, type Options } from 'zod-to-json-schema';
|
|
2
|
+
import { z, type ZodTypeAny } from 'zod';
|
|
3
|
+
import type { JsonSchema } from '../core/types.js';
|
|
4
|
+
|
|
5
|
+
// Options are optional; wrap in Partial to allow calling with {} or nothing.
|
|
6
|
+
export type McpSchemaOptions = Partial<Options> & {
|
|
7
|
+
/**
|
|
8
|
+
* Fallback value used when normalizing non-boolean additionalProperties.
|
|
9
|
+
* MCP requires boolean; default is true for permissive maps.
|
|
10
|
+
*/
|
|
11
|
+
additionalPropertiesFallback?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Removes "$schema" fields from generated JSON Schema.
|
|
14
|
+
* Enabled by default to keep MCP payloads compact and uniform.
|
|
15
|
+
*/
|
|
16
|
+
stripMetaSchema?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deeply normalizes a JSON Schema so every `additionalProperties` is boolean.
|
|
21
|
+
* Keep exported in this file to centralize MCP compatibility tweaks and make
|
|
22
|
+
* future converter upgrades (e.g., if upstream fixes this) easy.
|
|
23
|
+
*/
|
|
24
|
+
export function fixAdditionalProperties<T>(schema: T, fallback = true): T {
|
|
25
|
+
const clone = cloneSchema(schema);
|
|
26
|
+
return normalize(clone, fallback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Recursively unwraps a Zod schema to check if its core type is a ZodArray.
|
|
31
|
+
* This is necessary because methods like .describe() wrap the schema in a
|
|
32
|
+
* ZodEffects object, which hides the underlying type.
|
|
33
|
+
*/
|
|
34
|
+
export function isZodArray(schema: ZodTypeAny): boolean {
|
|
35
|
+
if (schema instanceof z.ZodArray) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (schema instanceof z.ZodEffects) {
|
|
39
|
+
return isZodArray(schema.innerType());
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a Zod schema to JSON Schema and enforces MCP's requirement
|
|
46
|
+
* that every `additionalProperties` field is a boolean.
|
|
47
|
+
*
|
|
48
|
+
* If the schema is an array type (even if wrapped), it will be wrapped
|
|
49
|
+
* in an object with a 'result' property.
|
|
50
|
+
*/
|
|
51
|
+
export function zodToMcpJsonSchema(
|
|
52
|
+
schema: ZodTypeAny,
|
|
53
|
+
options?: McpSchemaOptions,
|
|
54
|
+
) {
|
|
55
|
+
const {
|
|
56
|
+
additionalPropertiesFallback = true,
|
|
57
|
+
stripMetaSchema = true,
|
|
58
|
+
...converterOptions
|
|
59
|
+
} = options ?? {};
|
|
60
|
+
const jsonSchema = zodToJsonSchema(schema, converterOptions as Options | undefined) as JsonSchema;
|
|
61
|
+
|
|
62
|
+
const normalized = isZodArray(schema)
|
|
63
|
+
? fixAdditionalProperties(
|
|
64
|
+
{
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
result: jsonSchema,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
additionalPropertiesFallback,
|
|
71
|
+
)
|
|
72
|
+
: fixAdditionalProperties(jsonSchema, additionalPropertiesFallback);
|
|
73
|
+
|
|
74
|
+
return stripMetaSchema ? removeMetaSchema(normalized) : normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default zodToMcpJsonSchema;
|
|
78
|
+
|
|
79
|
+
function removeMetaSchema<T>(node: T): T {
|
|
80
|
+
if (Array.isArray(node)) {
|
|
81
|
+
return node.map((item) => removeMetaSchema(item)) as T;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (node === null || typeof node !== 'object') {
|
|
85
|
+
return node;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result: Record<string, unknown> = {};
|
|
89
|
+
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
|
90
|
+
if (key === '$schema') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
result[key] = removeMetaSchema(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result as T;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalize(node: unknown, fallback: boolean): any {
|
|
100
|
+
if (Array.isArray(node)) {
|
|
101
|
+
return node.map((child) => normalize(child, fallback));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (node === null || typeof node !== 'object') {
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result: Record<string, unknown> = { ...(node as Record<string, unknown>) };
|
|
109
|
+
|
|
110
|
+
if ('additionalProperties' in result && typeof result.additionalProperties !== 'boolean') {
|
|
111
|
+
result.additionalProperties = fallback;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const key of Object.keys(result)) {
|
|
115
|
+
if (key === 'additionalProperties') continue;
|
|
116
|
+
result[key] = normalize(result[key], fallback);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cloneSchema<T>(schema: T): T {
|
|
123
|
+
if (typeof structuredClone === 'function') {
|
|
124
|
+
return structuredClone(schema);
|
|
125
|
+
}
|
|
126
|
+
return JSON.parse(JSON.stringify(schema));
|
|
127
|
+
}
|