toonify-mcp 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 +344 -0
- package/README.zh-TW.md +324 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/metrics-collector.d.ts +40 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +93 -0
- package/dist/metrics/metrics-collector.js.map +1 -0
- package/dist/optimizer/token-optimizer.d.ts +34 -0
- package/dist/optimizer/token-optimizer.d.ts.map +1 -0
- package/dist/optimizer/token-optimizer.js +186 -0
- package/dist/optimizer/token-optimizer.js.map +1 -0
- package/dist/optimizer/types.d.ts +42 -0
- package/dist/optimizer/types.d.ts.map +1 -0
- package/dist/optimizer/types.js +5 -0
- package/dist/optimizer/types.js.map +1 -0
- package/dist/server/mcp-server.d.ts +12 -0
- package/dist/server/mcp-server.d.ts.map +1 -0
- package/dist/server/mcp-server.js +111 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/hooks/README.md +138 -0
- package/hooks/package.json +23 -0
- package/hooks/post-tool-use.js +161 -0
- package/hooks/post-tool-use.ts +201 -0
- package/hooks/tsconfig.json +18 -0
- package/jest.config.js +23 -0
- package/package.json +45 -0
- package/src/index.ts +23 -0
- package/src/metrics/metrics-collector.ts +125 -0
- package/src/optimizer/token-optimizer.ts +214 -0
- package/src/optimizer/types.ts +46 -0
- package/src/server/mcp-server.ts +134 -0
- package/test-mcp.json +5 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetricsCollector: Local-only metrics tracking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import type { TokenStats } from '../optimizer/types.js';
|
|
9
|
+
|
|
10
|
+
export interface OptimizationMetric {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
originalTokens: number;
|
|
14
|
+
optimizedTokens: number;
|
|
15
|
+
savings: number;
|
|
16
|
+
savingsPercentage: number;
|
|
17
|
+
wasOptimized: boolean;
|
|
18
|
+
format?: string;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MetricsCollector {
|
|
23
|
+
private statsPath: string;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
// Store in user's home directory, not in project
|
|
27
|
+
this.statsPath = path.join(
|
|
28
|
+
os.homedir(),
|
|
29
|
+
'.claude',
|
|
30
|
+
'token_stats.json'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record an optimization attempt
|
|
36
|
+
*/
|
|
37
|
+
async record(metric: OptimizationMetric): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
const stats = await this.loadStats();
|
|
40
|
+
|
|
41
|
+
// Update aggregated stats
|
|
42
|
+
stats.totalRequests++;
|
|
43
|
+
if (metric.wasOptimized) {
|
|
44
|
+
stats.optimizedRequests++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
stats.tokensBeforeOptimization += metric.originalTokens;
|
|
48
|
+
stats.tokensAfterOptimization += metric.optimizedTokens;
|
|
49
|
+
stats.totalSavings += metric.savings;
|
|
50
|
+
|
|
51
|
+
// Recalculate average
|
|
52
|
+
stats.averageSavingsPercentage =
|
|
53
|
+
stats.optimizedRequests > 0
|
|
54
|
+
? (stats.totalSavings / stats.tokensBeforeOptimization) * 100
|
|
55
|
+
: 0;
|
|
56
|
+
|
|
57
|
+
await this.saveStats(stats);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Silent failure - metrics should never break functionality
|
|
60
|
+
console.error('Failed to record metrics:', error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get current statistics
|
|
66
|
+
*/
|
|
67
|
+
async getStats(): Promise<TokenStats> {
|
|
68
|
+
return await this.loadStats();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load stats from disk
|
|
73
|
+
*/
|
|
74
|
+
private async loadStats(): Promise<TokenStats> {
|
|
75
|
+
try {
|
|
76
|
+
// Ensure directory exists
|
|
77
|
+
await fs.mkdir(path.dirname(this.statsPath), { recursive: true });
|
|
78
|
+
|
|
79
|
+
const data = await fs.readFile(this.statsPath, 'utf-8');
|
|
80
|
+
return JSON.parse(data);
|
|
81
|
+
} catch {
|
|
82
|
+
// Return empty stats if file doesn't exist
|
|
83
|
+
return {
|
|
84
|
+
totalRequests: 0,
|
|
85
|
+
optimizedRequests: 0,
|
|
86
|
+
tokensBeforeOptimization: 0,
|
|
87
|
+
tokensAfterOptimization: 0,
|
|
88
|
+
totalSavings: 0,
|
|
89
|
+
averageSavingsPercentage: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Save stats to disk
|
|
96
|
+
*/
|
|
97
|
+
private async saveStats(stats: TokenStats): Promise<void> {
|
|
98
|
+
await fs.writeFile(
|
|
99
|
+
this.statsPath,
|
|
100
|
+
JSON.stringify(stats, null, 2),
|
|
101
|
+
'utf-8'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format stats as dashboard
|
|
107
|
+
*/
|
|
108
|
+
async formatDashboard(): Promise<string> {
|
|
109
|
+
const stats = await this.getStats();
|
|
110
|
+
|
|
111
|
+
return `
|
|
112
|
+
📊 Token Optimization Stats
|
|
113
|
+
━━━━━━━━━━━━━━━━━━━━━━━━
|
|
114
|
+
Total Requests: ${stats.totalRequests}
|
|
115
|
+
Optimized: ${stats.optimizedRequests} (${((stats.optimizedRequests / stats.totalRequests) * 100).toFixed(1)}%)
|
|
116
|
+
|
|
117
|
+
Tokens Before: ${stats.tokensBeforeOptimization.toLocaleString()}
|
|
118
|
+
Tokens After: ${stats.tokensAfterOptimization.toLocaleString()}
|
|
119
|
+
Total Savings: ${stats.totalSavings.toLocaleString()} (${stats.averageSavingsPercentage.toFixed(1)}%)
|
|
120
|
+
|
|
121
|
+
💰 Cost Savings (at $3/1M input tokens):
|
|
122
|
+
$${((stats.totalSavings / 1_000_000) * 3).toFixed(2)} saved
|
|
123
|
+
`.trim();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenOptimizer: Core optimization logic using Toonify
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { encode as toonEncode, decode as toonDecode } from '@toon-format/toon';
|
|
6
|
+
import { encoding_for_model } from 'tiktoken';
|
|
7
|
+
import yaml from 'yaml';
|
|
8
|
+
import type {
|
|
9
|
+
OptimizationResult,
|
|
10
|
+
ToolMetadata,
|
|
11
|
+
StructuredData,
|
|
12
|
+
OptimizationConfig
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export class TokenOptimizer {
|
|
16
|
+
private config: OptimizationConfig;
|
|
17
|
+
private tokenEncoder;
|
|
18
|
+
|
|
19
|
+
constructor(config: Partial<OptimizationConfig> = {}) {
|
|
20
|
+
this.config = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
minTokensThreshold: 50,
|
|
23
|
+
minSavingsThreshold: 30,
|
|
24
|
+
maxProcessingTime: 50,
|
|
25
|
+
skipToolPatterns: [],
|
|
26
|
+
...config
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Use Claude tokenizer
|
|
30
|
+
this.tokenEncoder = encoding_for_model('gpt-4');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Main optimization method
|
|
35
|
+
*/
|
|
36
|
+
async optimize(
|
|
37
|
+
content: string,
|
|
38
|
+
metadata?: ToolMetadata
|
|
39
|
+
): Promise<OptimizationResult> {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
|
|
42
|
+
// Quick path: skip if disabled or content too small
|
|
43
|
+
if (!this.config.enabled || content.length < 200) {
|
|
44
|
+
return {
|
|
45
|
+
optimized: false,
|
|
46
|
+
originalContent: content,
|
|
47
|
+
originalTokens: this.countTokens(content),
|
|
48
|
+
reason: 'Content too small'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skip if tool matches skip patterns
|
|
53
|
+
if (metadata?.toolName && this.shouldSkipTool(metadata.toolName)) {
|
|
54
|
+
return {
|
|
55
|
+
optimized: false,
|
|
56
|
+
originalContent: content,
|
|
57
|
+
originalTokens: this.countTokens(content),
|
|
58
|
+
reason: `Tool ${metadata.toolName} in skip list`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Detect structured data
|
|
63
|
+
const structuredData = this.detectStructuredData(content);
|
|
64
|
+
if (!structuredData) {
|
|
65
|
+
return {
|
|
66
|
+
optimized: false,
|
|
67
|
+
originalContent: content,
|
|
68
|
+
originalTokens: this.countTokens(content),
|
|
69
|
+
reason: 'Not structured data'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Convert to TOON format
|
|
75
|
+
const toonContent = toonEncode(structuredData.data);
|
|
76
|
+
|
|
77
|
+
// Count tokens
|
|
78
|
+
const originalTokens = this.countTokens(content);
|
|
79
|
+
const optimizedTokens = this.countTokens(toonContent);
|
|
80
|
+
|
|
81
|
+
// Calculate savings
|
|
82
|
+
const tokenSavings = originalTokens - optimizedTokens;
|
|
83
|
+
const savingsPercentage = (tokenSavings / originalTokens) * 100;
|
|
84
|
+
|
|
85
|
+
// Check if worth using
|
|
86
|
+
if (savingsPercentage < this.config.minSavingsThreshold) {
|
|
87
|
+
return {
|
|
88
|
+
optimized: false,
|
|
89
|
+
originalContent: content,
|
|
90
|
+
originalTokens,
|
|
91
|
+
reason: `Savings too low: ${savingsPercentage.toFixed(1)}%`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check processing time
|
|
96
|
+
const elapsed = Date.now() - startTime;
|
|
97
|
+
if (elapsed > this.config.maxProcessingTime) {
|
|
98
|
+
return {
|
|
99
|
+
optimized: false,
|
|
100
|
+
originalContent: content,
|
|
101
|
+
originalTokens,
|
|
102
|
+
reason: `Processing timeout: ${elapsed}ms`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
optimized: true,
|
|
108
|
+
originalContent: content,
|
|
109
|
+
optimizedContent: toonContent,
|
|
110
|
+
originalTokens,
|
|
111
|
+
optimizedTokens,
|
|
112
|
+
savings: {
|
|
113
|
+
tokens: tokenSavings,
|
|
114
|
+
percentage: savingsPercentage
|
|
115
|
+
},
|
|
116
|
+
format: structuredData.type
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Silent fallback on error
|
|
121
|
+
return {
|
|
122
|
+
optimized: false,
|
|
123
|
+
originalContent: content,
|
|
124
|
+
originalTokens: this.countTokens(content),
|
|
125
|
+
reason: `Error: ${error instanceof Error ? error.message : 'Unknown'}`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Detect if content is structured data (JSON/CSV/YAML)
|
|
132
|
+
*/
|
|
133
|
+
private detectStructuredData(content: string): StructuredData | null {
|
|
134
|
+
// Try JSON first
|
|
135
|
+
try {
|
|
136
|
+
const data = JSON.parse(content);
|
|
137
|
+
if (typeof data === 'object' && data !== null) {
|
|
138
|
+
return { type: 'json', data, confidence: 1.0 };
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
|
|
142
|
+
// Try YAML
|
|
143
|
+
try {
|
|
144
|
+
const data = yaml.parse(content);
|
|
145
|
+
if (typeof data === 'object' && data !== null) {
|
|
146
|
+
return { type: 'yaml', data, confidence: 0.9 };
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
// Try CSV (simple heuristic)
|
|
151
|
+
if (this.looksLikeCSV(content)) {
|
|
152
|
+
try {
|
|
153
|
+
const data = this.parseSimpleCSV(content);
|
|
154
|
+
return { type: 'csv', data, confidence: 0.8 };
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Simple CSV detection heuristic
|
|
163
|
+
*/
|
|
164
|
+
private looksLikeCSV(content: string): boolean {
|
|
165
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
166
|
+
if (lines.length < 2) return false;
|
|
167
|
+
|
|
168
|
+
const firstLineCommas = (lines[0].match(/,/g) || []).length;
|
|
169
|
+
if (firstLineCommas === 0) return false;
|
|
170
|
+
|
|
171
|
+
// Check if most lines have similar comma count
|
|
172
|
+
let matchingLines = 0;
|
|
173
|
+
for (let i = 1; i < Math.min(lines.length, 10); i++) {
|
|
174
|
+
const commas = (lines[i].match(/,/g) || []).length;
|
|
175
|
+
if (commas === firstLineCommas) matchingLines++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return matchingLines >= Math.min(lines.length - 1, 7);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse simple CSV to array of objects
|
|
183
|
+
*/
|
|
184
|
+
private parseSimpleCSV(content: string): any[] {
|
|
185
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
186
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
187
|
+
|
|
188
|
+
return lines.slice(1).map(line => {
|
|
189
|
+
const values = line.split(',').map(v => v.trim());
|
|
190
|
+
const obj: any = {};
|
|
191
|
+
headers.forEach((header, i) => {
|
|
192
|
+
obj[header] = values[i] || '';
|
|
193
|
+
});
|
|
194
|
+
return obj;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Count tokens in text
|
|
200
|
+
*/
|
|
201
|
+
private countTokens(text: string): number {
|
|
202
|
+
return this.tokenEncoder.encode(text).length;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if tool should be skipped
|
|
207
|
+
*/
|
|
208
|
+
private shouldSkipTool(toolName: string): boolean {
|
|
209
|
+
return this.config.skipToolPatterns?.some(pattern => {
|
|
210
|
+
const regex = new RegExp(pattern);
|
|
211
|
+
return regex.test(toolName);
|
|
212
|
+
}) ?? false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for token optimization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface OptimizationResult {
|
|
6
|
+
optimized: boolean;
|
|
7
|
+
originalContent: string;
|
|
8
|
+
optimizedContent?: string;
|
|
9
|
+
originalTokens: number;
|
|
10
|
+
optimizedTokens?: number;
|
|
11
|
+
savings?: {
|
|
12
|
+
tokens: number;
|
|
13
|
+
percentage: number;
|
|
14
|
+
};
|
|
15
|
+
format?: 'json' | 'csv' | 'yaml' | 'unknown';
|
|
16
|
+
reason?: string; // Why optimization was skipped
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolMetadata {
|
|
20
|
+
toolName: string;
|
|
21
|
+
contentType?: string;
|
|
22
|
+
size: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StructuredData {
|
|
26
|
+
type: 'json' | 'csv' | 'yaml';
|
|
27
|
+
data: any;
|
|
28
|
+
confidence: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OptimizationConfig {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
minTokensThreshold: number; // Only optimize if content > N tokens
|
|
34
|
+
minSavingsThreshold: number; // Only use if savings > N%
|
|
35
|
+
maxProcessingTime: number; // Max ms to spend optimizing
|
|
36
|
+
skipToolPatterns?: string[]; // Tool names to skip
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TokenStats {
|
|
40
|
+
totalRequests: number;
|
|
41
|
+
optimizedRequests: number;
|
|
42
|
+
tokensBeforeOptimization: number;
|
|
43
|
+
tokensAfterOptimization: number;
|
|
44
|
+
totalSavings: number;
|
|
45
|
+
averageSavingsPercentage: number;
|
|
46
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToonifyMCPServer: MCP server that wraps tool results with token optimization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { TokenOptimizer } from '../optimizer/token-optimizer.js';
|
|
12
|
+
import { MetricsCollector } from '../metrics/metrics-collector.js';
|
|
13
|
+
|
|
14
|
+
export class ToonifyMCPServer {
|
|
15
|
+
private server: Server;
|
|
16
|
+
private optimizer: TokenOptimizer;
|
|
17
|
+
private metrics: MetricsCollector;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.server = new Server(
|
|
21
|
+
{
|
|
22
|
+
name: 'claude-code-toonify',
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this.optimizer = new TokenOptimizer();
|
|
33
|
+
this.metrics = new MetricsCollector();
|
|
34
|
+
|
|
35
|
+
this.setupHandlers();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private setupHandlers() {
|
|
39
|
+
// List available tools
|
|
40
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: 'optimize_content',
|
|
44
|
+
description: 'Optimize structured data content for token efficiency using TOON format',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
content: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'The content to optimize (JSON, CSV, or YAML)',
|
|
51
|
+
},
|
|
52
|
+
toolName: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Name of the tool that generated this content',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['content'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'get_stats',
|
|
62
|
+
description: 'Get token optimization statistics',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// Handle tool calls
|
|
72
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
73
|
+
const { name, arguments: args } = request.params;
|
|
74
|
+
|
|
75
|
+
switch (name) {
|
|
76
|
+
case 'optimize_content': {
|
|
77
|
+
const { content, toolName } = args as {
|
|
78
|
+
content: string;
|
|
79
|
+
toolName?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await this.optimizer.optimize(content, {
|
|
83
|
+
toolName: toolName || 'unknown',
|
|
84
|
+
size: content.length,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Record metrics
|
|
88
|
+
await this.metrics.record({
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
toolName: toolName || 'unknown',
|
|
91
|
+
originalTokens: result.originalTokens,
|
|
92
|
+
optimizedTokens: result.optimizedTokens || result.originalTokens,
|
|
93
|
+
savings: result.savings?.tokens || 0,
|
|
94
|
+
savingsPercentage: result.savings?.percentage || 0,
|
|
95
|
+
wasOptimized: result.optimized,
|
|
96
|
+
format: result.format,
|
|
97
|
+
reason: result.reason,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: JSON.stringify(result, null, 2),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'get_stats': {
|
|
111
|
+
const stats = await this.metrics.getStats();
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: JSON.stringify(stats, null, 2),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async start() {
|
|
129
|
+
const transport = new StdioServerTransport();
|
|
130
|
+
await this.server.connect(transport);
|
|
131
|
+
|
|
132
|
+
console.error('Toonify MCP Server running on stdio');
|
|
133
|
+
}
|
|
134
|
+
}
|
package/test-mcp.json
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|