@grafema/mcp 0.1.0-alpha.1

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/src/utils.ts ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * MCP Server Utilities
3
+ */
4
+
5
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'fs';
6
+ import { join, basename } from 'path';
7
+ import type { PaginationParams, ToolResult } from './types.js';
8
+
9
+ // === CONSTANTS ===
10
+ export const DEFAULT_LIMIT = 10;
11
+ export const MAX_LIMIT = 500;
12
+ export const DEFAULT_RESPONSE_SIZE_LIMIT = 100_000;
13
+
14
+ // === LOGGING ===
15
+ let logsDir: string | null = null;
16
+ const MAX_LOG_FILES = 7; // Keep logs for 7 days
17
+
18
+ function getLogFilePath(): string {
19
+ const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
20
+ const dir = logsDir || '/tmp';
21
+ return join(dir, `mcp-${date}.log`);
22
+ }
23
+
24
+ function cleanupOldLogs(): void {
25
+ if (!logsDir || !existsSync(logsDir)) return;
26
+
27
+ try {
28
+ const files = readdirSync(logsDir)
29
+ .filter(f => f.startsWith('mcp-') && f.endsWith('.log'))
30
+ .map(f => ({
31
+ name: f,
32
+ path: join(logsDir!, f),
33
+ mtime: statSync(join(logsDir!, f)).mtime.getTime()
34
+ }))
35
+ .sort((a, b) => b.mtime - a.mtime); // newest first
36
+
37
+ // Remove files beyond MAX_LOG_FILES
38
+ for (const file of files.slice(MAX_LOG_FILES)) {
39
+ unlinkSync(file.path);
40
+ }
41
+ } catch {
42
+ // Ignore cleanup errors
43
+ }
44
+ }
45
+
46
+ export function initLogger(grafemaDir: string): void {
47
+ logsDir = join(grafemaDir, 'logs');
48
+ if (!existsSync(logsDir)) {
49
+ mkdirSync(logsDir, { recursive: true });
50
+ }
51
+ cleanupOldLogs();
52
+ log(`[Grafema MCP] Logging initialized: ${logsDir}`);
53
+ }
54
+
55
+ export function getLogsDir(): string | null {
56
+ return logsDir;
57
+ }
58
+
59
+ export function log(msg: string): void {
60
+ const timestamp = new Date().toISOString();
61
+ const line = `[${timestamp}] ${msg}\n`;
62
+ try {
63
+ appendFileSync(getLogFilePath(), line);
64
+ } catch {
65
+ // Fallback to /tmp if logs dir not initialized
66
+ appendFileSync('/tmp/grafema-mcp.log', line);
67
+ }
68
+ }
69
+
70
+ // === PAGINATION ===
71
+ export function guardResponseSize(text: string, maxSize: number = DEFAULT_RESPONSE_SIZE_LIMIT): string {
72
+ if (text.length > maxSize) {
73
+ const truncated = text.slice(0, maxSize);
74
+ const remaining = text.length - maxSize;
75
+ return truncated + `\n\n... [TRUNCATED: ${remaining.toLocaleString()} chars remaining. Use limit/offset for pagination]`;
76
+ }
77
+ return text;
78
+ }
79
+
80
+ export function normalizeLimit(limit: number | undefined | null): number {
81
+ if (limit === undefined || limit === null) return DEFAULT_LIMIT;
82
+ return Math.min(Math.max(1, Math.floor(limit)), MAX_LIMIT);
83
+ }
84
+
85
+ export function formatPaginationInfo(params: PaginationParams): string {
86
+ const { limit, offset, returned, total, hasMore } = params;
87
+ let info = `\nšŸ“„ Pagination: showing ${returned}`;
88
+ if (total !== undefined) {
89
+ info += ` of ${total}`;
90
+ }
91
+ if (offset > 0) {
92
+ info += ` (offset: ${offset})`;
93
+ }
94
+ if (hasMore) {
95
+ info += ` — use offset=${offset + returned} to get more`;
96
+ }
97
+ return info;
98
+ }
99
+
100
+ // === TYPE HELPERS ===
101
+ export function findSimilarTypes(
102
+ queriedType: string,
103
+ availableTypes: string[],
104
+ maxDistance: number = 2
105
+ ): string[] {
106
+ const queriedLower = queriedType.toLowerCase();
107
+ const similar: string[] = [];
108
+
109
+ for (const type of availableTypes) {
110
+ const dist = levenshtein(queriedLower, type.toLowerCase());
111
+ if (dist > 0 && dist <= maxDistance) {
112
+ similar.push(type);
113
+ }
114
+ }
115
+
116
+ return similar;
117
+ }
118
+
119
+ // Levenshtein distance implementation
120
+ export function levenshtein(a: string, b: string): number {
121
+ const m = a.length;
122
+ const n = b.length;
123
+
124
+ if (m === 0) return n;
125
+ if (n === 0) return m;
126
+
127
+ const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
128
+
129
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
130
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
131
+
132
+ for (let i = 1; i <= m; i++) {
133
+ for (let j = 1; j <= n; j++) {
134
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
135
+ dp[i][j] = Math.min(
136
+ dp[i - 1][j] + 1,
137
+ dp[i][j - 1] + 1,
138
+ dp[i - 1][j - 1] + cost
139
+ );
140
+ }
141
+ }
142
+
143
+ return dp[m][n];
144
+ }
145
+
146
+ // === SERIALIZATION ===
147
+ export function serializeBigInt(obj: unknown): unknown {
148
+ if (obj === null || obj === undefined) return obj;
149
+ if (typeof obj === 'bigint') return obj.toString();
150
+ if (Array.isArray(obj)) return obj.map(serializeBigInt);
151
+ if (typeof obj === 'object') {
152
+ const result: Record<string, unknown> = {};
153
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
154
+ result[key] = serializeBigInt(value);
155
+ }
156
+ return result;
157
+ }
158
+ return obj;
159
+ }
160
+
161
+ // === TOOL RESULT HELPERS ===
162
+ export function textResult(text: string): ToolResult {
163
+ return {
164
+ content: [{ type: 'text', text }],
165
+ };
166
+ }
167
+
168
+ export function errorResult(message: string): ToolResult {
169
+ return {
170
+ content: [{ type: 'text', text: `Error: ${message}` }],
171
+ isError: true,
172
+ };
173
+ }
174
+
175
+ export function jsonResult(data: unknown, pretty: boolean = true): ToolResult {
176
+ const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
177
+ return textResult(text);
178
+ }
179
+
180
+ // === PROCESS HELPERS ===
181
+ export function isProcessRunning(pid: number): boolean {
182
+ try {
183
+ process.kill(pid, 0);
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ // === FORMATTING ===
191
+ export function formatDuration(ms: number): string {
192
+ if (ms < 1000) return `${ms}ms`;
193
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
194
+ return `${(ms / 60000).toFixed(1)}m`;
195
+ }
196
+
197
+ export function formatNumber(n: number): string {
198
+ return n.toLocaleString();
199
+ }
200
+
201
+ export function truncate(s: string, maxLen: number): string {
202
+ if (s.length <= maxLen) return s;
203
+ return s.slice(0, maxLen - 3) + '...';
204
+ }