mcp-ts-template 1.1.6
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 +201 -0
- package/README.md +233 -0
- package/dist/config/index.d.ts +73 -0
- package/dist/config/index.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +162 -0
- package/dist/mcp-client/client.d.ts +36 -0
- package/dist/mcp-client/client.js +276 -0
- package/dist/mcp-client/configLoader.d.ts +75 -0
- package/dist/mcp-client/configLoader.js +203 -0
- package/dist/mcp-client/index.d.ts +10 -0
- package/dist/mcp-client/index.js +14 -0
- package/dist/mcp-client/transport.d.ts +34 -0
- package/dist/mcp-client/transport.js +183 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
- package/dist/mcp-server/resources/echoResource/index.js +5 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
- package/dist/mcp-server/resources/echoResource/registration.js +122 -0
- package/dist/mcp-server/server.d.ts +27 -0
- package/dist/mcp-server/server.js +176 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
- package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
- package/dist/mcp-server/tools/echoTool/index.js +5 -0
- package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
- package/dist/mcp-server/tools/echoTool/registration.js +86 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +411 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
- package/dist/mcp-server/transports/stdioTransport.js +70 -0
- package/dist/types-global/errors.d.ts +73 -0
- package/dist/types-global/errors.js +66 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/internal/errorHandler.d.ts +90 -0
- package/dist/utils/internal/errorHandler.js +247 -0
- package/dist/utils/internal/index.d.ts +3 -0
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.d.ts +50 -0
- package/dist/utils/internal/logger.js +267 -0
- package/dist/utils/internal/requestContext.d.ts +47 -0
- package/dist/utils/internal/requestContext.js +48 -0
- package/dist/utils/metrics/index.d.ts +1 -0
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/metrics/tokenCounter.d.ts +27 -0
- package/dist/utils/metrics/tokenCounter.js +124 -0
- package/dist/utils/parsing/dateParser.d.ts +27 -0
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.d.ts +2 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/parsing/jsonParser.d.ts +46 -0
- package/dist/utils/parsing/jsonParser.js +79 -0
- package/dist/utils/security/idGenerator.d.ts +93 -0
- package/dist/utils/security/idGenerator.js +147 -0
- package/dist/utils/security/index.d.ts +3 -0
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/security/rateLimiter.d.ts +92 -0
- package/dist/utils/security/rateLimiter.js +171 -0
- package/dist/utils/security/sanitization.d.ts +180 -0
- package/dist/utils/security/sanitization.js +372 -0
- package/package.json +79 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import winston from 'winston';
|
|
5
|
+
import { config } from '../../config/index.js';
|
|
6
|
+
// Define the numeric severity for comparison (lower is more severe)
|
|
7
|
+
const mcpLevelSeverity = {
|
|
8
|
+
emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7
|
|
9
|
+
};
|
|
10
|
+
// Map MCP levels to Winston's core levels for file logging
|
|
11
|
+
const mcpToWinstonLevel = {
|
|
12
|
+
debug: 'debug',
|
|
13
|
+
info: 'info',
|
|
14
|
+
notice: 'info', // Map notice to info for file logging
|
|
15
|
+
warning: 'warn',
|
|
16
|
+
error: 'error',
|
|
17
|
+
crit: 'error', // Map critical levels to error for file logging
|
|
18
|
+
alert: 'error',
|
|
19
|
+
emerg: 'error',
|
|
20
|
+
};
|
|
21
|
+
// Resolve __dirname for ESM
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
// Calculate project root robustly (works from src/ or dist/)
|
|
25
|
+
const isRunningFromDist = __dirname.includes(path.sep + 'dist' + path.sep);
|
|
26
|
+
const levelsToGoUp = isRunningFromDist ? 3 : 2;
|
|
27
|
+
const pathSegments = Array(levelsToGoUp).fill('..');
|
|
28
|
+
const projectRoot = path.resolve(__dirname, ...pathSegments);
|
|
29
|
+
const logsDir = path.join(projectRoot, 'logs');
|
|
30
|
+
// Security: ensure logsDir is within projectRoot
|
|
31
|
+
const resolvedLogsDir = path.resolve(logsDir);
|
|
32
|
+
const isLogsDirSafe = resolvedLogsDir === projectRoot || resolvedLogsDir.startsWith(projectRoot + path.sep);
|
|
33
|
+
if (!isLogsDirSafe) {
|
|
34
|
+
// Use console.error here as logger might not be initialized or safe
|
|
35
|
+
console.error(`FATAL: logs directory "${resolvedLogsDir}" is outside project root "${projectRoot}". File logging disabled.`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Singleton Logger wrapping Winston, adapted for MCP.
|
|
39
|
+
* Logs to files and optionally sends MCP notifications/message.
|
|
40
|
+
*/
|
|
41
|
+
class Logger {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.initialized = false;
|
|
44
|
+
this.currentMcpLevel = 'info'; // Default MCP level
|
|
45
|
+
this.currentWinstonLevel = 'info'; // Default Winston level
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Initialize Winston logger for file transport. Must be called once at app start.
|
|
49
|
+
* Console transport is added conditionally.
|
|
50
|
+
* @param level Initial minimum level to log ('info' default).
|
|
51
|
+
*/
|
|
52
|
+
async initialize(level = 'info') {
|
|
53
|
+
if (this.initialized) {
|
|
54
|
+
// Avoid console.warn in stdio mode, this will be logged via this.info later if needed.
|
|
55
|
+
if (process.stdout.isTTY) {
|
|
56
|
+
console.warn('Logger already initialized.');
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.currentMcpLevel = level;
|
|
61
|
+
this.currentWinstonLevel = mcpToWinstonLevel[level];
|
|
62
|
+
// Ensure logs directory exists
|
|
63
|
+
if (isLogsDirSafe) {
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(resolvedLogsDir)) {
|
|
66
|
+
fs.mkdirSync(resolvedLogsDir, { recursive: true });
|
|
67
|
+
// Avoid console.log in stdio mode. This info will be part of the initialization log message.
|
|
68
|
+
// if (process.stdout.isTTY) {
|
|
69
|
+
// console.log(`Created logs directory: ${resolvedLogsDir}`);
|
|
70
|
+
// }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(`Error creating logs directory at ${resolvedLogsDir}: ${err.message}. File logging disabled.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Common format for files
|
|
78
|
+
const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
|
|
79
|
+
const transports = [];
|
|
80
|
+
// Add file transports only if the directory is safe
|
|
81
|
+
if (isLogsDirSafe) {
|
|
82
|
+
transports.push(new winston.transports.File({ filename: path.join(resolvedLogsDir, 'error.log'), level: 'error', format: fileFormat }), new winston.transports.File({ filename: path.join(resolvedLogsDir, 'warn.log'), level: 'warn', format: fileFormat }), new winston.transports.File({ filename: path.join(resolvedLogsDir, 'info.log'), level: 'info', format: fileFormat }), new winston.transports.File({ filename: path.join(resolvedLogsDir, 'debug.log'), level: 'debug', format: fileFormat }), new winston.transports.File({ filename: path.join(resolvedLogsDir, 'combined.log'), format: fileFormat }));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Avoid console.warn in stdio mode. This info will be part of the initialization log message.
|
|
86
|
+
// if (process.stdout.isTTY) {
|
|
87
|
+
// console.warn("File logging disabled due to unsafe logs directory path.");
|
|
88
|
+
// }
|
|
89
|
+
}
|
|
90
|
+
// Conditionally add Console transport only if:
|
|
91
|
+
// 1. MCP level is 'debug'
|
|
92
|
+
// 2. stdout is a TTY (interactive terminal, not piped)
|
|
93
|
+
if (this.currentMcpLevel === 'debug' && process.stdout.isTTY) {
|
|
94
|
+
const consoleFormat = winston.format.combine(winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
95
|
+
let metaString = '';
|
|
96
|
+
const metaCopy = { ...meta };
|
|
97
|
+
if (metaCopy.error && typeof metaCopy.error === 'object') {
|
|
98
|
+
const errorObj = metaCopy.error;
|
|
99
|
+
if (errorObj.message)
|
|
100
|
+
metaString += `\n Error: ${errorObj.message}`;
|
|
101
|
+
if (errorObj.stack)
|
|
102
|
+
metaString += `\n Stack: ${String(errorObj.stack).split('\n').map((l) => ` ${l}`).join('\n')}`;
|
|
103
|
+
delete metaCopy.error;
|
|
104
|
+
}
|
|
105
|
+
if (Object.keys(metaCopy).length > 0) {
|
|
106
|
+
try {
|
|
107
|
+
const remainingMetaJson = JSON.stringify(metaCopy, null, 2);
|
|
108
|
+
if (remainingMetaJson !== '{}')
|
|
109
|
+
metaString += `\n Meta: ${remainingMetaJson}`;
|
|
110
|
+
}
|
|
111
|
+
catch (stringifyError) {
|
|
112
|
+
metaString += `\n Meta: [Error stringifying metadata: ${stringifyError.message}]`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return `${timestamp} ${level}: ${message}${metaString}`;
|
|
116
|
+
}));
|
|
117
|
+
transports.push(new winston.transports.Console({
|
|
118
|
+
level: 'debug',
|
|
119
|
+
format: consoleFormat,
|
|
120
|
+
}));
|
|
121
|
+
// This log will go through winston itself if console transport is added.
|
|
122
|
+
// If not, it shouldn't go to console.
|
|
123
|
+
// console.log(`Console logging enabled at level: debug (stdout is TTY)`);
|
|
124
|
+
}
|
|
125
|
+
else if (this.currentMcpLevel === 'debug' && !process.stdout.isTTY) {
|
|
126
|
+
// Avoid console.log in stdio mode. This info will be part of the initialization log message.
|
|
127
|
+
// console.log(`Console logging skipped: Level is debug, but stdout is not a TTY (likely stdio transport).`);
|
|
128
|
+
}
|
|
129
|
+
// Create logger with the initial Winston level and configured transports
|
|
130
|
+
this.winstonLogger = winston.createLogger({
|
|
131
|
+
level: this.currentWinstonLevel,
|
|
132
|
+
transports,
|
|
133
|
+
exitOnError: false
|
|
134
|
+
});
|
|
135
|
+
this.initialized = true;
|
|
136
|
+
await Promise.resolve(); // Yield to event loop
|
|
137
|
+
this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${process.stdout.isTTY && this.currentMcpLevel === 'debug' ? 'enabled' : 'disabled'}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Sets the function used to send MCP 'notifications/message'.
|
|
141
|
+
*/
|
|
142
|
+
setMcpNotificationSender(sender) {
|
|
143
|
+
this.mcpNotificationSender = sender;
|
|
144
|
+
const status = sender ? 'enabled' : 'disabled';
|
|
145
|
+
this.info(`MCP notification sending ${status}.`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Dynamically sets the minimum logging level.
|
|
149
|
+
*/
|
|
150
|
+
setLevel(newLevel) {
|
|
151
|
+
if (!this.ensureInitialized()) {
|
|
152
|
+
console.error("Cannot set level: Logger not initialized.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!(newLevel in mcpLevelSeverity)) {
|
|
156
|
+
this.warning(`Invalid MCP log level provided: ${newLevel}. Level not changed.`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const oldLevel = this.currentMcpLevel;
|
|
160
|
+
this.currentMcpLevel = newLevel;
|
|
161
|
+
this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
|
|
162
|
+
this.winstonLogger.level = this.currentWinstonLevel;
|
|
163
|
+
// Add or remove console transport based on the new level and TTY status
|
|
164
|
+
const consoleTransport = this.winstonLogger.transports.find(t => t instanceof winston.transports.Console);
|
|
165
|
+
const shouldHaveConsole = newLevel === 'debug' && process.stdout.isTTY;
|
|
166
|
+
if (shouldHaveConsole && !consoleTransport) {
|
|
167
|
+
// Add console transport
|
|
168
|
+
const consoleFormat = winston.format.combine( /* ... same format as in initialize ... */); // TODO: Extract format to avoid duplication
|
|
169
|
+
this.winstonLogger.add(new winston.transports.Console({ level: 'debug', format: consoleFormat }));
|
|
170
|
+
this.info('Console logging dynamically enabled.');
|
|
171
|
+
}
|
|
172
|
+
else if (!shouldHaveConsole && consoleTransport) {
|
|
173
|
+
// Remove console transport
|
|
174
|
+
this.winstonLogger.remove(consoleTransport);
|
|
175
|
+
this.info('Console logging dynamically disabled.');
|
|
176
|
+
}
|
|
177
|
+
if (oldLevel !== newLevel) {
|
|
178
|
+
this.info(`Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${shouldHaveConsole ? 'enabled' : 'disabled'}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Get singleton instance. */
|
|
182
|
+
static getInstance() {
|
|
183
|
+
if (!Logger.instance) {
|
|
184
|
+
Logger.instance = new Logger();
|
|
185
|
+
}
|
|
186
|
+
return Logger.instance;
|
|
187
|
+
}
|
|
188
|
+
/** Ensures the logger has been initialized. */
|
|
189
|
+
ensureInitialized() {
|
|
190
|
+
if (!this.initialized || !this.winstonLogger) {
|
|
191
|
+
// Avoid console.warn in stdio mode.
|
|
192
|
+
// if (process.stdout.isTTY) {
|
|
193
|
+
// console.warn('Logger not initialized; message dropped.');
|
|
194
|
+
// }
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/** Centralized log processing */
|
|
200
|
+
log(level, msg, context, error) {
|
|
201
|
+
if (!this.ensureInitialized())
|
|
202
|
+
return;
|
|
203
|
+
if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const logData = { ...context };
|
|
207
|
+
const winstonLevel = mcpToWinstonLevel[level];
|
|
208
|
+
if (error) {
|
|
209
|
+
this.winstonLogger.log(winstonLevel, msg, { ...logData, error: error });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
this.winstonLogger.log(winstonLevel, msg, logData);
|
|
213
|
+
}
|
|
214
|
+
if (this.mcpNotificationSender) {
|
|
215
|
+
const mcpDataPayload = { message: msg };
|
|
216
|
+
if (context)
|
|
217
|
+
mcpDataPayload.context = context;
|
|
218
|
+
if (error) {
|
|
219
|
+
mcpDataPayload.error = { message: error.message };
|
|
220
|
+
if (this.currentMcpLevel === 'debug' && error.stack) {
|
|
221
|
+
mcpDataPayload.error.stack = error.stack.substring(0, 500);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
this.mcpNotificationSender(level, mcpDataPayload, config.mcpServerName);
|
|
226
|
+
}
|
|
227
|
+
catch (sendError) {
|
|
228
|
+
this.winstonLogger.error("Failed to send MCP log notification", {
|
|
229
|
+
originalLevel: level,
|
|
230
|
+
originalMessage: msg,
|
|
231
|
+
sendError: sendError instanceof Error ? sendError.message : String(sendError),
|
|
232
|
+
mcpPayload: mcpDataPayload
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// --- Public Logging Methods ---
|
|
238
|
+
debug(msg, context) { this.log('debug', msg, context); }
|
|
239
|
+
info(msg, context) { this.log('info', msg, context); }
|
|
240
|
+
notice(msg, context) { this.log('notice', msg, context); }
|
|
241
|
+
warning(msg, context) { this.log('warning', msg, context); }
|
|
242
|
+
error(msg, err, context) {
|
|
243
|
+
const errorObj = err instanceof Error ? err : undefined;
|
|
244
|
+
const combinedContext = err instanceof Error ? context : { ...(err || {}), ...(context || {}) };
|
|
245
|
+
this.log('error', msg, combinedContext, errorObj);
|
|
246
|
+
}
|
|
247
|
+
crit(msg, err, context) {
|
|
248
|
+
const errorObj = err instanceof Error ? err : undefined;
|
|
249
|
+
const combinedContext = err instanceof Error ? context : { ...(err || {}), ...(context || {}) };
|
|
250
|
+
this.log('crit', msg, combinedContext, errorObj);
|
|
251
|
+
}
|
|
252
|
+
alert(msg, err, context) {
|
|
253
|
+
const errorObj = err instanceof Error ? err : undefined;
|
|
254
|
+
const combinedContext = err instanceof Error ? context : { ...(err || {}), ...(context || {}) };
|
|
255
|
+
this.log('alert', msg, combinedContext, errorObj);
|
|
256
|
+
}
|
|
257
|
+
emerg(msg, err, context) {
|
|
258
|
+
const errorObj = err instanceof Error ? err : undefined;
|
|
259
|
+
const combinedContext = err instanceof Error ? context : { ...(err || {}), ...(context || {}) };
|
|
260
|
+
this.log('emerg', msg, combinedContext, errorObj);
|
|
261
|
+
}
|
|
262
|
+
fatal(msg, context, error) {
|
|
263
|
+
this.log('emerg', msg, context, error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Export singleton instance
|
|
267
|
+
export const logger = Logger.getInstance();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the structure for context information associated with a request or operation.
|
|
3
|
+
*/
|
|
4
|
+
export interface RequestContext {
|
|
5
|
+
/** Unique identifier generated for the request context instance. */
|
|
6
|
+
requestId: string;
|
|
7
|
+
/** ISO 8601 timestamp indicating when the context was created. */
|
|
8
|
+
timestamp: string;
|
|
9
|
+
/** Allows for additional, arbitrary key-value pairs for specific context needs. */
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Configuration interface for request context utilities
|
|
14
|
+
*/
|
|
15
|
+
export interface ContextConfig {
|
|
16
|
+
/** Custom configuration properties */
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Operation context with request data
|
|
21
|
+
*/
|
|
22
|
+
export interface OperationContext {
|
|
23
|
+
/** Request context data */
|
|
24
|
+
requestContext?: RequestContext;
|
|
25
|
+
/** Custom context properties */
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
export declare const requestContextService: {
|
|
29
|
+
config: ContextConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Configure service settings
|
|
32
|
+
* @param config New configuration
|
|
33
|
+
* @returns Updated configuration
|
|
34
|
+
*/
|
|
35
|
+
configure(config: Partial<ContextConfig>): ContextConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Get current configuration
|
|
38
|
+
* @returns Current configuration
|
|
39
|
+
*/
|
|
40
|
+
getConfig(): ContextConfig;
|
|
41
|
+
/**
|
|
42
|
+
* Create a request context with unique ID and timestamp
|
|
43
|
+
* @param additionalContext Additional context properties
|
|
44
|
+
* @returns Request context object
|
|
45
|
+
*/
|
|
46
|
+
createRequestContext(additionalContext?: Record<string, unknown>): RequestContext;
|
|
47
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
// Import utils from the main barrel file (generateUUID from ../security/idGenerator.js)
|
|
3
|
+
import { generateUUID } from '../index.js';
|
|
4
|
+
// Direct instance for request context utilities
|
|
5
|
+
const requestContextServiceInstance = {
|
|
6
|
+
config: {},
|
|
7
|
+
/**
|
|
8
|
+
* Configure service settings
|
|
9
|
+
* @param config New configuration
|
|
10
|
+
* @returns Updated configuration
|
|
11
|
+
*/
|
|
12
|
+
configure(config) {
|
|
13
|
+
this.config = {
|
|
14
|
+
...this.config,
|
|
15
|
+
...config
|
|
16
|
+
};
|
|
17
|
+
logger.debug('RequestContext configuration updated', { config: this.config });
|
|
18
|
+
return { ...this.config };
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Get current configuration
|
|
22
|
+
* @returns Current configuration
|
|
23
|
+
*/
|
|
24
|
+
getConfig() {
|
|
25
|
+
return { ...this.config };
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Create a request context with unique ID and timestamp
|
|
29
|
+
* @param additionalContext Additional context properties
|
|
30
|
+
* @returns Request context object
|
|
31
|
+
*/
|
|
32
|
+
createRequestContext(additionalContext = {}) {
|
|
33
|
+
const requestId = generateUUID(); // Use imported generateUUID
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
return {
|
|
36
|
+
requestId,
|
|
37
|
+
timestamp,
|
|
38
|
+
...additionalContext
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
// generateSecureRandomString function removed as it was unused and redundant
|
|
42
|
+
};
|
|
43
|
+
// Export the instance directly
|
|
44
|
+
export const requestContextService = requestContextServiceInstance;
|
|
45
|
+
// Removed delegate functions and default export for simplicity.
|
|
46
|
+
// Users should import and use `requestContextService` directly.
|
|
47
|
+
// e.g., import { requestContextService } from './requestContext.js';
|
|
48
|
+
// requestContextService.createRequestContext();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './tokenCounter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './tokenCounter.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
2
|
+
import { RequestContext } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the number of tokens for a given text using the 'gpt-4o' tokenizer.
|
|
5
|
+
* Uses ErrorHandler for consistent error management.
|
|
6
|
+
*
|
|
7
|
+
* @param text - The input text to tokenize.
|
|
8
|
+
* @param context - Optional request context for logging and error handling.
|
|
9
|
+
* @returns The number of tokens.
|
|
10
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
11
|
+
*/
|
|
12
|
+
export declare function countTokens(text: string, context?: RequestContext): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* Calculates the number of tokens for chat messages using the ChatCompletionMessageParam structure
|
|
15
|
+
* and the 'gpt-4o' tokenizer, considering special tokens and message overhead.
|
|
16
|
+
* This implementation is based on OpenAI's guidelines for gpt-4/gpt-3.5-turbo models.
|
|
17
|
+
* Uses ErrorHandler for consistent error management.
|
|
18
|
+
*
|
|
19
|
+
* See: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
|
20
|
+
*
|
|
21
|
+
* @param messages - An array of chat messages in the `ChatCompletionMessageParam` format.
|
|
22
|
+
* @param context - Optional request context for logging and error handling.
|
|
23
|
+
* @returns The estimated number of tokens.
|
|
24
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
25
|
+
*/
|
|
26
|
+
export declare function countChatTokens(messages: ReadonlyArray<ChatCompletionMessageParam>, // Use the complex type
|
|
27
|
+
context?: RequestContext): Promise<number>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { encoding_for_model } from 'tiktoken';
|
|
2
|
+
import { BaseErrorCode } from '../../types-global/errors.js';
|
|
3
|
+
// Import utils from the main barrel file (ErrorHandler, logger, RequestContext from ../internal/*)
|
|
4
|
+
import { ErrorHandler, logger } from '../index.js';
|
|
5
|
+
// Define the model used specifically for token counting
|
|
6
|
+
const TOKENIZATION_MODEL = 'gpt-4o'; // Note this is strictly for token counting, not the model used for inference
|
|
7
|
+
/**
|
|
8
|
+
* Calculates the number of tokens for a given text using the 'gpt-4o' tokenizer.
|
|
9
|
+
* Uses ErrorHandler for consistent error management.
|
|
10
|
+
*
|
|
11
|
+
* @param text - The input text to tokenize.
|
|
12
|
+
* @param context - Optional request context for logging and error handling.
|
|
13
|
+
* @returns The number of tokens.
|
|
14
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
15
|
+
*/
|
|
16
|
+
export async function countTokens(text, context) {
|
|
17
|
+
// Wrap the synchronous operation in tryCatch which handles both sync/async
|
|
18
|
+
return ErrorHandler.tryCatch(() => {
|
|
19
|
+
let encoding = null;
|
|
20
|
+
try {
|
|
21
|
+
// Always use the defined TOKENIZATION_MODEL
|
|
22
|
+
encoding = encoding_for_model(TOKENIZATION_MODEL);
|
|
23
|
+
const tokens = encoding.encode(text);
|
|
24
|
+
return tokens.length;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
encoding?.free(); // Ensure the encoder is freed if it was successfully created
|
|
28
|
+
}
|
|
29
|
+
}, {
|
|
30
|
+
operation: 'countTokens',
|
|
31
|
+
context: context,
|
|
32
|
+
input: { textSample: text.substring(0, 50) + '...' }, // Log sanitized input
|
|
33
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR for external lib issues
|
|
34
|
+
rethrow: true // Rethrow as McpError
|
|
35
|
+
// Removed onErrorReturn as we now rethrow
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculates the number of tokens for chat messages using the ChatCompletionMessageParam structure
|
|
40
|
+
* and the 'gpt-4o' tokenizer, considering special tokens and message overhead.
|
|
41
|
+
* This implementation is based on OpenAI's guidelines for gpt-4/gpt-3.5-turbo models.
|
|
42
|
+
* Uses ErrorHandler for consistent error management.
|
|
43
|
+
*
|
|
44
|
+
* See: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
|
45
|
+
*
|
|
46
|
+
* @param messages - An array of chat messages in the `ChatCompletionMessageParam` format.
|
|
47
|
+
* @param context - Optional request context for logging and error handling.
|
|
48
|
+
* @returns The estimated number of tokens.
|
|
49
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
50
|
+
*/
|
|
51
|
+
export async function countChatTokens(messages, // Use the complex type
|
|
52
|
+
context) {
|
|
53
|
+
// Wrap the synchronous operation in tryCatch
|
|
54
|
+
return ErrorHandler.tryCatch(() => {
|
|
55
|
+
let encoding = null;
|
|
56
|
+
let num_tokens = 0;
|
|
57
|
+
try {
|
|
58
|
+
// Always use the defined TOKENIZATION_MODEL
|
|
59
|
+
encoding = encoding_for_model(TOKENIZATION_MODEL);
|
|
60
|
+
// Define tokens per message/name based on gpt-4o (same as gpt-4/gpt-3.5-turbo)
|
|
61
|
+
const tokens_per_message = 3;
|
|
62
|
+
const tokens_per_name = 1;
|
|
63
|
+
for (const message of messages) {
|
|
64
|
+
num_tokens += tokens_per_message;
|
|
65
|
+
// Encode role
|
|
66
|
+
num_tokens += encoding.encode(message.role).length;
|
|
67
|
+
// Encode content - handle potential null or array content (vision)
|
|
68
|
+
if (typeof message.content === 'string') {
|
|
69
|
+
num_tokens += encoding.encode(message.content).length;
|
|
70
|
+
}
|
|
71
|
+
else if (Array.isArray(message.content)) {
|
|
72
|
+
// Handle multi-part content (e.g., text + image) - simplified: encode text parts only
|
|
73
|
+
for (const part of message.content) {
|
|
74
|
+
if (part.type === 'text') {
|
|
75
|
+
num_tokens += encoding.encode(part.text).length;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Add placeholder token count for non-text parts (e.g., images) if needed
|
|
79
|
+
// This requires specific model knowledge (e.g., OpenAI vision model token costs)
|
|
80
|
+
logger.warning(`Non-text content part found (type: ${part.type}), token count contribution ignored.`, context);
|
|
81
|
+
// num_tokens += IMAGE_TOKEN_COST; // Placeholder
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} // else: content is null, add 0 tokens
|
|
85
|
+
// Encode name if present (often associated with 'tool' or 'function' roles in newer models)
|
|
86
|
+
if ('name' in message && message.name) {
|
|
87
|
+
num_tokens += tokens_per_name;
|
|
88
|
+
num_tokens += encoding.encode(message.name).length;
|
|
89
|
+
}
|
|
90
|
+
// --- Handle tool calls (specific to newer models) ---
|
|
91
|
+
// Assistant message requesting tool calls
|
|
92
|
+
if (message.role === 'assistant' && 'tool_calls' in message && message.tool_calls) {
|
|
93
|
+
for (const tool_call of message.tool_calls) {
|
|
94
|
+
// Add tokens for the function name and arguments
|
|
95
|
+
if (tool_call.function.name) {
|
|
96
|
+
num_tokens += encoding.encode(tool_call.function.name).length;
|
|
97
|
+
}
|
|
98
|
+
if (tool_call.function.arguments) {
|
|
99
|
+
// Arguments are often JSON strings
|
|
100
|
+
num_tokens += encoding.encode(tool_call.function.arguments).length;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Tool message providing results
|
|
105
|
+
if (message.role === 'tool' && 'tool_call_id' in message && message.tool_call_id) {
|
|
106
|
+
num_tokens += encoding.encode(message.tool_call_id).length;
|
|
107
|
+
// Content of the tool message (the result) is already handled by the string content check above
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
num_tokens += 3; // every reply is primed with <|start|>assistant<|message|>
|
|
111
|
+
return num_tokens;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
encoding?.free();
|
|
115
|
+
}
|
|
116
|
+
}, {
|
|
117
|
+
operation: 'countChatTokens',
|
|
118
|
+
context: context,
|
|
119
|
+
input: { messageCount: messages.length }, // Log sanitized input
|
|
120
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR
|
|
121
|
+
rethrow: true // Rethrow as McpError
|
|
122
|
+
// Removed onErrorReturn
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as chrono from 'chrono-node';
|
|
2
|
+
import { RequestContext } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses a natural language date string into a Date object.
|
|
5
|
+
*
|
|
6
|
+
* @param text The natural language date string (e.g., "tomorrow", "in 5 days", "2024-01-15").
|
|
7
|
+
* @param context The request context for logging and error tracking.
|
|
8
|
+
* @param refDate Optional reference date for parsing relative dates. Defaults to now.
|
|
9
|
+
* @returns A Date object representing the parsed date, or null if parsing fails.
|
|
10
|
+
* @throws McpError if parsing fails unexpectedly.
|
|
11
|
+
*/
|
|
12
|
+
declare function parseDateString(text: string, context: RequestContext, refDate?: Date): Promise<Date | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Parses a natural language date string and returns detailed parsing results.
|
|
15
|
+
*
|
|
16
|
+
* @param text The natural language date string.
|
|
17
|
+
* @param context The request context for logging and error tracking.
|
|
18
|
+
* @param refDate Optional reference date for parsing relative dates. Defaults to now.
|
|
19
|
+
* @returns An array of chrono.ParsedResult objects, or an empty array if parsing fails.
|
|
20
|
+
* @throws McpError if parsing fails unexpectedly.
|
|
21
|
+
*/
|
|
22
|
+
declare function parseDateStringDetailed(text: string, context: RequestContext, refDate?: Date): Promise<chrono.ParsedResult[]>;
|
|
23
|
+
export declare const dateParser: {
|
|
24
|
+
parse: typeof parseDateStringDetailed;
|
|
25
|
+
parseDate: typeof parseDateString;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as chrono from 'chrono-node';
|
|
2
|
+
// Import utils from the main barrel file (logger, ErrorHandler, RequestContext from ../internal/*)
|
|
3
|
+
import { logger, ErrorHandler } from '../index.js';
|
|
4
|
+
import { BaseErrorCode } from '../../types-global/errors.js'; // Corrected path
|
|
5
|
+
/**
|
|
6
|
+
* Parses a natural language date string into a Date object.
|
|
7
|
+
*
|
|
8
|
+
* @param text The natural language date string (e.g., "tomorrow", "in 5 days", "2024-01-15").
|
|
9
|
+
* @param context The request context for logging and error tracking.
|
|
10
|
+
* @param refDate Optional reference date for parsing relative dates. Defaults to now.
|
|
11
|
+
* @returns A Date object representing the parsed date, or null if parsing fails.
|
|
12
|
+
* @throws McpError if parsing fails unexpectedly.
|
|
13
|
+
*/
|
|
14
|
+
async function parseDateString(text, context, refDate) {
|
|
15
|
+
const operation = 'parseDateString';
|
|
16
|
+
const logContext = { ...context, operation, inputText: text, refDate };
|
|
17
|
+
logger.debug(`Attempting to parse date string: "${text}"`, logContext);
|
|
18
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
19
|
+
const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true });
|
|
20
|
+
if (parsedDate) {
|
|
21
|
+
logger.debug(`Successfully parsed "${text}" to ${parsedDate.toISOString()}`, logContext);
|
|
22
|
+
return parsedDate;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger.warning(`Failed to parse date string: "${text}"`, logContext);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}, {
|
|
29
|
+
operation,
|
|
30
|
+
context: logContext,
|
|
31
|
+
input: { text, refDate },
|
|
32
|
+
errorCode: BaseErrorCode.PARSING_ERROR,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parses a natural language date string and returns detailed parsing results.
|
|
37
|
+
*
|
|
38
|
+
* @param text The natural language date string.
|
|
39
|
+
* @param context The request context for logging and error tracking.
|
|
40
|
+
* @param refDate Optional reference date for parsing relative dates. Defaults to now.
|
|
41
|
+
* @returns An array of chrono.ParsedResult objects, or an empty array if parsing fails.
|
|
42
|
+
* @throws McpError if parsing fails unexpectedly.
|
|
43
|
+
*/
|
|
44
|
+
async function parseDateStringDetailed(text, context, refDate) {
|
|
45
|
+
const operation = 'parseDateStringDetailed';
|
|
46
|
+
const logContext = { ...context, operation, inputText: text, refDate };
|
|
47
|
+
logger.debug(`Attempting detailed parse of date string: "${text}"`, logContext);
|
|
48
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
49
|
+
const results = chrono.parse(text, refDate, { forwardDate: true });
|
|
50
|
+
logger.debug(`Detailed parse of "${text}" resulted in ${results.length} result(s)`, logContext);
|
|
51
|
+
return results;
|
|
52
|
+
}, {
|
|
53
|
+
operation,
|
|
54
|
+
context: logContext,
|
|
55
|
+
input: { text, refDate },
|
|
56
|
+
errorCode: BaseErrorCode.PARSING_ERROR,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export const dateParser = {
|
|
60
|
+
parse: parseDateStringDetailed,
|
|
61
|
+
parseDate: parseDateString,
|
|
62
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RequestContext } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Enum mirroring partial-json's Allow constants for specifying
|
|
4
|
+
* what types of partial JSON structures are permissible during parsing.
|
|
5
|
+
* Use bitwise OR to combine options (e.g., Allow.STR | Allow.OBJ).
|
|
6
|
+
*/
|
|
7
|
+
export declare const Allow: {
|
|
8
|
+
STR: number;
|
|
9
|
+
NUM: number;
|
|
10
|
+
ARR: number;
|
|
11
|
+
OBJ: number;
|
|
12
|
+
NULL: number;
|
|
13
|
+
BOOL: number;
|
|
14
|
+
NAN: number;
|
|
15
|
+
INFINITY: number;
|
|
16
|
+
_INFINITY: number;
|
|
17
|
+
INF: number;
|
|
18
|
+
SPECIAL: number;
|
|
19
|
+
ATOM: number;
|
|
20
|
+
COLLECTION: number;
|
|
21
|
+
ALL: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Utility class for parsing potentially partial JSON strings.
|
|
25
|
+
* Wraps the 'partial-json' library to provide a consistent interface
|
|
26
|
+
* within the atlas-mcp-agent project.
|
|
27
|
+
* Handles optional <think>...</think> blocks at the beginning of the input.
|
|
28
|
+
*/
|
|
29
|
+
declare class JsonParser {
|
|
30
|
+
/**
|
|
31
|
+
* Parses a JSON string, potentially allowing for incomplete structures
|
|
32
|
+
* and handling optional <think> blocks at the start.
|
|
33
|
+
*
|
|
34
|
+
* @param jsonString The JSON string to parse.
|
|
35
|
+
* @param allowPartial A bitwise OR combination of 'Allow' constants specifying permissible partial types (defaults to Allow.ALL).
|
|
36
|
+
* @param context Optional RequestContext for error correlation and logging think blocks.
|
|
37
|
+
* @returns The parsed JavaScript value.
|
|
38
|
+
* @throws {McpError} Throws an McpError with BaseErrorCode.VALIDATION_ERROR if parsing fails due to malformed JSON.
|
|
39
|
+
*/
|
|
40
|
+
parse<T = any>(jsonString: string, allowPartial?: number, context?: RequestContext): T;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Singleton instance of the JsonParser utility.
|
|
44
|
+
*/
|
|
45
|
+
export declare const jsonParser: JsonParser;
|
|
46
|
+
export {};
|