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.
@@ -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
@@ -0,0 +1,5 @@
1
+ {
2
+ "jsonrpc": "2.0",
3
+ "id": 1,
4
+ "method": "tools/list"
5
+ }
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
+ }