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
package/hooks/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Claude Code Toonify Hook
|
|
2
|
+
|
|
3
|
+
Automatic TOON format optimization hook for Claude Code CLI.
|
|
4
|
+
|
|
5
|
+
## What This Does
|
|
6
|
+
|
|
7
|
+
This hook **automatically intercepts** tool results from Read, Grep, and other file operations, detects structured data (JSON/CSV/YAML), and applies TOON format optimization when token savings > 30%.
|
|
8
|
+
|
|
9
|
+
**No manual calls required** - optimization happens transparently.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### 1. Build the hook
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd hooks/
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Register with Claude Code
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# From hooks/ directory
|
|
25
|
+
npm run install-hook
|
|
26
|
+
|
|
27
|
+
# Or manually:
|
|
28
|
+
claude hooks add PostToolUse -- node /Users/ktseng/Developer/Projects/toonify-mcp/hooks/post-tool-use.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Verify installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
claude hooks list
|
|
35
|
+
# Should show: PostToolUse: node .../post-tool-use.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Create `~/.claude/toonify-hook-config.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"minTokensThreshold": 50,
|
|
46
|
+
"minSavingsThreshold": 30,
|
|
47
|
+
"skipToolPatterns": ["Bash", "Write", "Edit"]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
|
|
53
|
+
- `enabled`: Enable/disable automatic optimization (default: true)
|
|
54
|
+
- `minTokensThreshold`: Minimum tokens before optimization (default: 50)
|
|
55
|
+
- `minSavingsThreshold`: Minimum savings percentage required (default: 30%)
|
|
56
|
+
- `skipToolPatterns`: Tools to never optimize (default: ["Bash", "Write", "Edit"])
|
|
57
|
+
|
|
58
|
+
### Environment Variables
|
|
59
|
+
|
|
60
|
+
- `TOONIFY_SHOW_STATS=true` - Show optimization stats in output
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
User: Read large JSON file
|
|
66
|
+
↓
|
|
67
|
+
Claude Code calls Read tool
|
|
68
|
+
↓
|
|
69
|
+
PostToolUse hook intercepts result
|
|
70
|
+
↓
|
|
71
|
+
Hook detects JSON, converts to TOON
|
|
72
|
+
↓
|
|
73
|
+
Optimized content sent to Claude API
|
|
74
|
+
↓
|
|
75
|
+
60% token reduction achieved
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## vs MCP Server
|
|
79
|
+
|
|
80
|
+
| Feature | MCP Server | PostToolUse Hook |
|
|
81
|
+
|---------|-----------|------------------|
|
|
82
|
+
| **Activation** | Manual (call tool) | Automatic (intercept) |
|
|
83
|
+
| **Compatibility** | Any MCP client | Claude Code only |
|
|
84
|
+
| **Configuration** | MCP tools | Hook config file |
|
|
85
|
+
| **Performance** | Call overhead | Zero overhead |
|
|
86
|
+
|
|
87
|
+
**Recommendation**: Use PostToolUse hook for automatic optimization in Claude Code. Use MCP server for explicit control or other MCP clients.
|
|
88
|
+
|
|
89
|
+
## Uninstall
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
claude hooks remove PostToolUse
|
|
93
|
+
rm ~/.claude/toonify-hook-config.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Troubleshooting
|
|
97
|
+
|
|
98
|
+
### Hook not triggering
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Check if hook is registered
|
|
102
|
+
claude hooks list
|
|
103
|
+
|
|
104
|
+
# Check configuration
|
|
105
|
+
cat ~/.claude/toonify-hook-config.json
|
|
106
|
+
|
|
107
|
+
# Test manually
|
|
108
|
+
echo '{"toolName":"Read","toolResult":{"content":[{"type":"text","text":"{\"test\":123}"}]}}' | node post-tool-use.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Optimization not applied
|
|
112
|
+
|
|
113
|
+
- Check `minTokensThreshold` - content might be too small
|
|
114
|
+
- Check `minSavingsThreshold` - savings might be < 30%
|
|
115
|
+
- Check `skipToolPatterns` - tool might be in skip list
|
|
116
|
+
- Verify content is valid JSON/CSV/YAML
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
**Before (142 tokens)**:
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"products": [
|
|
124
|
+
{"id": 101, "name": "Laptop Pro", "price": 1299},
|
|
125
|
+
{"id": 102, "name": "Magic Mouse", "price": 79}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**After (57 tokens, -60%)**:
|
|
131
|
+
```
|
|
132
|
+
[TOON-JSON]
|
|
133
|
+
products[2]{id,name,price}:
|
|
134
|
+
101,Laptop Pro,1299
|
|
135
|
+
102,Magic Mouse,79
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Automatically applied** - no manual calls needed!
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toonify-mcp-hook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code hook for automatic TOON format optimization",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "post-tool-use.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"toonify-hook": "./post-tool-use.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@toon-format/toon": "^2.1.0",
|
|
12
|
+
"tiktoken": "^1.0.0",
|
|
13
|
+
"yaml": "^2.3.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.0.0",
|
|
17
|
+
"typescript": "^5.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"install-hook": "npm run build && claude hooks add PostToolUse -- node $(pwd)/post-tool-use.js"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code PostToolUse Hook for Automatic Toonify Optimization
|
|
4
|
+
*/
|
|
5
|
+
import { encode as toonEncode } from '@toon-format/toon';
|
|
6
|
+
import { encoding_for_model } from 'tiktoken';
|
|
7
|
+
import * as yaml from 'yaml';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
class ToonifyHook {
|
|
12
|
+
config;
|
|
13
|
+
encoder;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.config = this.loadConfig();
|
|
16
|
+
this.encoder = encoding_for_model('gpt-4');
|
|
17
|
+
}
|
|
18
|
+
loadConfig() {
|
|
19
|
+
const configPath = path.join(os.homedir(), '.claude', 'toonify-hook-config.json');
|
|
20
|
+
const defaultConfig = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
minTokensThreshold: 50,
|
|
23
|
+
minSavingsThreshold: 30,
|
|
24
|
+
skipToolPatterns: ['Bash', 'Write', 'Edit']
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(configPath)) {
|
|
28
|
+
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
29
|
+
return { ...defaultConfig, ...userConfig };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error('[Toonify Hook] Config load error:', error);
|
|
34
|
+
}
|
|
35
|
+
return defaultConfig;
|
|
36
|
+
}
|
|
37
|
+
shouldSkipTool(toolName) {
|
|
38
|
+
return this.config.skipToolPatterns.some(pattern => toolName.toLowerCase().includes(pattern.toLowerCase()));
|
|
39
|
+
}
|
|
40
|
+
countTokens(text) {
|
|
41
|
+
try {
|
|
42
|
+
return this.encoder.encode(text).length;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return Math.ceil(text.length / 4);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
detectStructuredData(content) {
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(content);
|
|
51
|
+
if (typeof data === 'object' && data !== null) {
|
|
52
|
+
return { type: 'json', data };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
try {
|
|
57
|
+
const data = yaml.parse(content);
|
|
58
|
+
if (typeof data === 'object' && data !== null && !content.includes(',')) {
|
|
59
|
+
return { type: 'yaml', data };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
const lines = content.trim().split('\n');
|
|
64
|
+
if (lines.length > 1) {
|
|
65
|
+
const firstLine = lines[0];
|
|
66
|
+
const delimiter = firstLine.includes('\t') ? '\t' : ',';
|
|
67
|
+
const columnCount = firstLine.split(delimiter).length;
|
|
68
|
+
if (columnCount > 1 && lines.every(line => line.split(delimiter).length === columnCount)) {
|
|
69
|
+
const headers = firstLine.split(delimiter);
|
|
70
|
+
const rows = lines.slice(1).map(line => {
|
|
71
|
+
const values = line.split(delimiter);
|
|
72
|
+
return headers.reduce((obj, header, i) => {
|
|
73
|
+
obj[header.trim()] = values[i]?.trim() || '';
|
|
74
|
+
return obj;
|
|
75
|
+
}, {});
|
|
76
|
+
});
|
|
77
|
+
return { type: 'csv', data: rows };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
optimize(content) {
|
|
83
|
+
if (!this.config.enabled) {
|
|
84
|
+
return { optimized: false, content };
|
|
85
|
+
}
|
|
86
|
+
const originalTokens = this.countTokens(content);
|
|
87
|
+
if (originalTokens < this.config.minTokensThreshold) {
|
|
88
|
+
return { optimized: false, content };
|
|
89
|
+
}
|
|
90
|
+
const structured = this.detectStructuredData(content);
|
|
91
|
+
if (!structured) {
|
|
92
|
+
return { optimized: false, content };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const toonContent = toonEncode(structured.data);
|
|
96
|
+
const optimizedTokens = this.countTokens(toonContent);
|
|
97
|
+
const savings = ((originalTokens - optimizedTokens) / originalTokens) * 100;
|
|
98
|
+
if (savings < this.config.minSavingsThreshold) {
|
|
99
|
+
return { optimized: false, content };
|
|
100
|
+
}
|
|
101
|
+
const optimizedContent = `[TOON-${structured.type.toUpperCase()}]\n${toonContent}`;
|
|
102
|
+
return {
|
|
103
|
+
optimized: true,
|
|
104
|
+
content: optimizedContent,
|
|
105
|
+
stats: {
|
|
106
|
+
originalTokens,
|
|
107
|
+
optimizedTokens,
|
|
108
|
+
savings: Math.round(savings)
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return { optimized: false, content };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async process(input) {
|
|
117
|
+
if (this.shouldSkipTool(input.toolName)) {
|
|
118
|
+
return input;
|
|
119
|
+
}
|
|
120
|
+
const processedContent = input.toolResult.content.map(block => {
|
|
121
|
+
if (block.type === 'text' && block.text) {
|
|
122
|
+
const result = this.optimize(block.text);
|
|
123
|
+
if (result.optimized && result.stats) {
|
|
124
|
+
const notice = `\n\n[Token Optimization: ${result.stats.originalTokens} → ${result.stats.optimizedTokens} tokens (-${result.stats.savings}%)]`;
|
|
125
|
+
return {
|
|
126
|
+
...block,
|
|
127
|
+
text: result.content + (process.env.TOONIFY_SHOW_STATS === 'true' ? notice : '')
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return block;
|
|
131
|
+
}
|
|
132
|
+
return block;
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
...input,
|
|
136
|
+
toolResult: {
|
|
137
|
+
...input.toolResult,
|
|
138
|
+
content: processedContent
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function main() {
|
|
144
|
+
try {
|
|
145
|
+
const stdin = await new Promise((resolve) => {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
process.stdin.on('data', chunk => chunks.push(chunk));
|
|
148
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
149
|
+
});
|
|
150
|
+
const input = JSON.parse(stdin);
|
|
151
|
+
const hook = new ToonifyHook();
|
|
152
|
+
const output = await hook.process(input);
|
|
153
|
+
process.stdout.write(JSON.stringify(output, null, 2));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
process.stdout.write(process.stdin.read());
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
main();
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code PostToolUse Hook for Automatic Toonify Optimization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { encode as toonEncode } from '@toon-format/toon';
|
|
7
|
+
import { encoding_for_model } from 'tiktoken';
|
|
8
|
+
import * as yaml from 'yaml';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
|
|
13
|
+
interface HookInput {
|
|
14
|
+
toolName: string;
|
|
15
|
+
toolInput: Record<string, unknown>;
|
|
16
|
+
toolResult: {
|
|
17
|
+
content: Array<{ type: string; text?: string }>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface OptimizationConfig {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
minTokensThreshold: number;
|
|
24
|
+
minSavingsThreshold: number;
|
|
25
|
+
skipToolPatterns: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class ToonifyHook {
|
|
29
|
+
private config: OptimizationConfig;
|
|
30
|
+
private encoder;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
this.config = this.loadConfig();
|
|
34
|
+
this.encoder = encoding_for_model('gpt-4');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private loadConfig(): OptimizationConfig {
|
|
38
|
+
const configPath = path.join(os.homedir(), '.claude', 'toonify-hook-config.json');
|
|
39
|
+
const defaultConfig: OptimizationConfig = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
minTokensThreshold: 50,
|
|
42
|
+
minSavingsThreshold: 30,
|
|
43
|
+
skipToolPatterns: ['Bash', 'Write', 'Edit']
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
49
|
+
return { ...defaultConfig, ...userConfig };
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[Toonify Hook] Config load error:', error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return defaultConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private shouldSkipTool(toolName: string): boolean {
|
|
59
|
+
return this.config.skipToolPatterns.some(pattern =>
|
|
60
|
+
toolName.toLowerCase().includes(pattern.toLowerCase())
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private countTokens(text: string): number {
|
|
65
|
+
try {
|
|
66
|
+
return this.encoder.encode(text).length;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return Math.ceil(text.length / 4);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private detectStructuredData(content: string): { type: 'json' | 'csv' | 'yaml'; data: any } | null {
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(content);
|
|
75
|
+
if (typeof data === 'object' && data !== null) {
|
|
76
|
+
return { type: 'json', data };
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const data = yaml.parse(content);
|
|
82
|
+
if (typeof data === 'object' && data !== null && !content.includes(',')) {
|
|
83
|
+
return { type: 'yaml', data };
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
|
|
87
|
+
const lines = content.trim().split('\n');
|
|
88
|
+
if (lines.length > 1) {
|
|
89
|
+
const firstLine = lines[0];
|
|
90
|
+
const delimiter = firstLine.includes('\t') ? '\t' : ',';
|
|
91
|
+
const columnCount = firstLine.split(delimiter).length;
|
|
92
|
+
|
|
93
|
+
if (columnCount > 1 && lines.every(line => line.split(delimiter).length === columnCount)) {
|
|
94
|
+
const headers = firstLine.split(delimiter);
|
|
95
|
+
const rows = lines.slice(1).map(line => {
|
|
96
|
+
const values = line.split(delimiter);
|
|
97
|
+
return headers.reduce((obj, header, i) => {
|
|
98
|
+
obj[header.trim()] = values[i]?.trim() || '';
|
|
99
|
+
return obj;
|
|
100
|
+
}, {} as Record<string, string>);
|
|
101
|
+
});
|
|
102
|
+
return { type: 'csv', data: rows };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private optimize(content: string): { optimized: boolean; content: string; stats?: { originalTokens: number; optimizedTokens: number; savings: number } } {
|
|
110
|
+
if (!this.config.enabled) {
|
|
111
|
+
return { optimized: false, content };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const originalTokens = this.countTokens(content);
|
|
115
|
+
if (originalTokens < this.config.minTokensThreshold) {
|
|
116
|
+
return { optimized: false, content };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const structured = this.detectStructuredData(content);
|
|
120
|
+
if (!structured) {
|
|
121
|
+
return { optimized: false, content };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const toonContent = toonEncode(structured.data);
|
|
126
|
+
const optimizedTokens = this.countTokens(toonContent);
|
|
127
|
+
const savings = ((originalTokens - optimizedTokens) / originalTokens) * 100;
|
|
128
|
+
|
|
129
|
+
if (savings < this.config.minSavingsThreshold) {
|
|
130
|
+
return { optimized: false, content };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const optimizedContent = `[TOON-${structured.type.toUpperCase()}]\n${toonContent}`;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
optimized: true,
|
|
137
|
+
content: optimizedContent,
|
|
138
|
+
stats: {
|
|
139
|
+
originalTokens,
|
|
140
|
+
optimizedTokens,
|
|
141
|
+
savings: Math.round(savings)
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { optimized: false, content };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async process(input: HookInput): Promise<HookInput> {
|
|
150
|
+
if (this.shouldSkipTool(input.toolName)) {
|
|
151
|
+
return input;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const processedContent = input.toolResult.content.map(block => {
|
|
155
|
+
if (block.type === 'text' && block.text) {
|
|
156
|
+
const result = this.optimize(block.text);
|
|
157
|
+
|
|
158
|
+
if (result.optimized && result.stats) {
|
|
159
|
+
const notice = `\n\n[Token Optimization: ${result.stats.originalTokens} → ${result.stats.optimizedTokens} tokens (-${result.stats.savings}%)]`;
|
|
160
|
+
return {
|
|
161
|
+
...block,
|
|
162
|
+
text: result.content + (process.env.TOONIFY_SHOW_STATS === 'true' ? notice : '')
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return block;
|
|
167
|
+
}
|
|
168
|
+
return block;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...input,
|
|
173
|
+
toolResult: {
|
|
174
|
+
...input.toolResult,
|
|
175
|
+
content: processedContent
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
try {
|
|
183
|
+
const stdin = await new Promise<string>((resolve) => {
|
|
184
|
+
const chunks: Buffer[] = [];
|
|
185
|
+
process.stdin.on('data', chunk => chunks.push(chunk));
|
|
186
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const input: HookInput = JSON.parse(stdin);
|
|
190
|
+
const hook = new ToonifyHook();
|
|
191
|
+
const output = await hook.process(input);
|
|
192
|
+
|
|
193
|
+
process.stdout.write(JSON.stringify(output, null, 2));
|
|
194
|
+
process.exit(0);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
process.stdout.write(process.stdin.read());
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"outDir": ".",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": false
|
|
15
|
+
},
|
|
16
|
+
"include": ["post-tool-use.ts"],
|
|
17
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
18
|
+
}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
preset: 'ts-jest/presets/default-esm',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
6
|
+
moduleNameMapper: {
|
|
7
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
8
|
+
},
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.tsx?$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
useESM: true,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
transformIgnorePatterns: [
|
|
18
|
+
'node_modules/(?!(@toon-format|tiktoken)/)',
|
|
19
|
+
],
|
|
20
|
+
testMatch: ['**/tests/**/*.test.ts'],
|
|
21
|
+
collectCoverageFrom: ['src/**/*.ts'],
|
|
22
|
+
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
|
|
23
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toonify-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for TOON format token optimization - works with any MCP client (Claude Code, ChatGPT, etc.)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"toonify-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
14
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
15
|
+
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
16
|
+
"prepare": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"mcp",
|
|
22
|
+
"token-optimization",
|
|
23
|
+
"toonify",
|
|
24
|
+
"llm-optimization"
|
|
25
|
+
],
|
|
26
|
+
"author": "ktseng",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"@toon-format/toon": "^2.1.0",
|
|
31
|
+
"tiktoken": "^1.0.0",
|
|
32
|
+
"yaml": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@jest/globals": "^30.2.0",
|
|
36
|
+
"@types/jest": "^29.0.0",
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"jest": "^29.0.0",
|
|
39
|
+
"ts-jest": "^29.4.6",
|
|
40
|
+
"typescript": "^5.3.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Toonify MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Optimizes token usage by converting structured data to TOON format
|
|
7
|
+
* before sending to Claude API, achieving 60%+ token reduction.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ToonifyMCPServer } from './server/mcp-server.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const server = new ToonifyMCPServer();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await server.start();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Failed to start Toonify MCP server:', error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main();
|