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.
Files changed (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/dist/config/index.d.ts +73 -0
  4. package/dist/config/index.js +125 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +162 -0
  7. package/dist/mcp-client/client.d.ts +36 -0
  8. package/dist/mcp-client/client.js +276 -0
  9. package/dist/mcp-client/configLoader.d.ts +75 -0
  10. package/dist/mcp-client/configLoader.js +203 -0
  11. package/dist/mcp-client/index.d.ts +10 -0
  12. package/dist/mcp-client/index.js +14 -0
  13. package/dist/mcp-client/transport.d.ts +34 -0
  14. package/dist/mcp-client/transport.js +183 -0
  15. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
  16. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
  17. package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
  18. package/dist/mcp-server/resources/echoResource/index.js +5 -0
  19. package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
  20. package/dist/mcp-server/resources/echoResource/registration.js +122 -0
  21. package/dist/mcp-server/server.d.ts +27 -0
  22. package/dist/mcp-server/server.js +176 -0
  23. package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
  24. package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
  25. package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
  26. package/dist/mcp-server/tools/echoTool/index.js +5 -0
  27. package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
  28. package/dist/mcp-server/tools/echoTool/registration.js +86 -0
  29. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
  30. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  31. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  32. package/dist/mcp-server/transports/httpTransport.js +411 -0
  33. package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
  34. package/dist/mcp-server/transports/stdioTransport.js +70 -0
  35. package/dist/types-global/errors.d.ts +73 -0
  36. package/dist/types-global/errors.js +66 -0
  37. package/dist/utils/index.d.ts +4 -0
  38. package/dist/utils/index.js +12 -0
  39. package/dist/utils/internal/errorHandler.d.ts +90 -0
  40. package/dist/utils/internal/errorHandler.js +247 -0
  41. package/dist/utils/internal/index.d.ts +3 -0
  42. package/dist/utils/internal/index.js +3 -0
  43. package/dist/utils/internal/logger.d.ts +50 -0
  44. package/dist/utils/internal/logger.js +267 -0
  45. package/dist/utils/internal/requestContext.d.ts +47 -0
  46. package/dist/utils/internal/requestContext.js +48 -0
  47. package/dist/utils/metrics/index.d.ts +1 -0
  48. package/dist/utils/metrics/index.js +1 -0
  49. package/dist/utils/metrics/tokenCounter.d.ts +27 -0
  50. package/dist/utils/metrics/tokenCounter.js +124 -0
  51. package/dist/utils/parsing/dateParser.d.ts +27 -0
  52. package/dist/utils/parsing/dateParser.js +62 -0
  53. package/dist/utils/parsing/index.d.ts +2 -0
  54. package/dist/utils/parsing/index.js +2 -0
  55. package/dist/utils/parsing/jsonParser.d.ts +46 -0
  56. package/dist/utils/parsing/jsonParser.js +79 -0
  57. package/dist/utils/security/idGenerator.d.ts +93 -0
  58. package/dist/utils/security/idGenerator.js +147 -0
  59. package/dist/utils/security/index.d.ts +3 -0
  60. package/dist/utils/security/index.js +3 -0
  61. package/dist/utils/security/rateLimiter.d.ts +92 -0
  62. package/dist/utils/security/rateLimiter.js +171 -0
  63. package/dist/utils/security/sanitization.d.ts +180 -0
  64. package/dist/utils/security/sanitization.js +372 -0
  65. 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,2 @@
1
+ export * from './jsonParser.js';
2
+ export * from './dateParser.js';
@@ -0,0 +1,2 @@
1
+ export * from './jsonParser.js';
2
+ export * from './dateParser.js';
@@ -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 {};