@ebowwa/large-output 1.0.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/README.md +168 -0
- package/index.ts +262 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @mcp/large-output
|
|
2
|
+
|
|
3
|
+
Shared utility for handling large MCP outputs with automatic file fallback.
|
|
4
|
+
|
|
5
|
+
## Problem Solved
|
|
6
|
+
|
|
7
|
+
When MCP tools return large outputs, they typically hit Claude's ~20,000 character tool output limit. Common solutions like pagination fragment the context and require multiple round-trips.
|
|
8
|
+
|
|
9
|
+
**This library solves it by:**
|
|
10
|
+
- Returning content inline if under the threshold
|
|
11
|
+
- Automatically writing to a temp file if over the threshold
|
|
12
|
+
- Returning a structured response with file path, size, and preview
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# From within an MCP package
|
|
18
|
+
bun add ../../large-output
|
|
19
|
+
|
|
20
|
+
# Or as a local dependency
|
|
21
|
+
# In package.json:
|
|
22
|
+
# "dependencies": {
|
|
23
|
+
# "@mcp/large-output": "file:../large-output"
|
|
24
|
+
# }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { handleMCPOutput } from "@mcp/large-output";
|
|
31
|
+
|
|
32
|
+
// In your MCP tool handler:
|
|
33
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
34
|
+
const { query } = request.params.arguments as { query: string };
|
|
35
|
+
|
|
36
|
+
// Fetch your data
|
|
37
|
+
const results = await fetchData(query);
|
|
38
|
+
const content = JSON.stringify(results, null, 2);
|
|
39
|
+
|
|
40
|
+
// Return with automatic file fallback
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: handleMCPOutput(content) }],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `handleMCPOutput(content, options?)`
|
|
50
|
+
|
|
51
|
+
Convenience function that handles output and returns MCP-formatted string.
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
|--------|------|---------|-------------|
|
|
55
|
+
| `threshold` | number | `15000` | Character threshold for file fallback |
|
|
56
|
+
| `previewLength` | number | `500` | Preview chars when written to file |
|
|
57
|
+
| `tempDir` | string | `os.tmpdir()` | Custom temp directory |
|
|
58
|
+
| `filenamePrefix` | string | `"mcp_output"` | Filename prefix |
|
|
59
|
+
| `includeSize` | boolean | `true` | Include size in file response |
|
|
60
|
+
| `includePreview` | boolean | `true` | Include preview in file response |
|
|
61
|
+
|
|
62
|
+
### Response Formats
|
|
63
|
+
|
|
64
|
+
**Inline (under threshold):**
|
|
65
|
+
```
|
|
66
|
+
<raw content returned directly>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**File (over threshold):**
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"type": "file",
|
|
73
|
+
"path": "/tmp/mcp_output_2025-02-04T12-38-00_abc123.txt",
|
|
74
|
+
"size": 45000,
|
|
75
|
+
"sizeFormatted": "45.0 KB",
|
|
76
|
+
"preview": "..."
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Advanced Usage
|
|
81
|
+
|
|
82
|
+
### Direct response handling
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { handleOutput, toMCPResponse } from "@mcp/large-output";
|
|
86
|
+
|
|
87
|
+
const response = handleOutput(largeContent, {
|
|
88
|
+
threshold: 20000,
|
|
89
|
+
filenamePrefix: "github_search",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (response.type === "file") {
|
|
93
|
+
console.log(`Written ${response.sizeFormatted} to ${response.path}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Convert to MCP return format
|
|
97
|
+
return toMCPResponse(response);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Batch handling
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { handleBatch } from "@mcp/large-output";
|
|
104
|
+
|
|
105
|
+
const outputs = handleBatch([data1, data2, data3]);
|
|
106
|
+
// Each output handled independently
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples by MCP Server
|
|
110
|
+
|
|
111
|
+
### github-search
|
|
112
|
+
```typescript
|
|
113
|
+
import { handleMCPOutput } from "@mcp/large-output";
|
|
114
|
+
|
|
115
|
+
const results = await githubApi.search(query, { per_page: 100 });
|
|
116
|
+
// Fetch all pages, accumulate results
|
|
117
|
+
return handleMCPOutput(JSON.stringify(results, null, 2));
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### claude-code-history
|
|
121
|
+
```typescript
|
|
122
|
+
import { handleMCPOutput } from "@mcp/large-output";
|
|
123
|
+
|
|
124
|
+
const conversations = await getConversations({ limit: 1000 });
|
|
125
|
+
return handleMCPOutput(JSON.stringify(conversations, null, 2));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### git
|
|
129
|
+
```typescript
|
|
130
|
+
import { handleMCPOutput } from "@mcp/large-output";
|
|
131
|
+
|
|
132
|
+
const commits = await gitLog({ maxCount: 500 });
|
|
133
|
+
return handleMCPOutput(JSON.stringify(commits, null, 2));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### npm-publish
|
|
137
|
+
```typescript
|
|
138
|
+
import { handleMCPOutput } from "@mcp/large-output";
|
|
139
|
+
|
|
140
|
+
const packages = await searchPackages({ limit: 500 });
|
|
141
|
+
return handleMCPOutput(JSON.stringify(packages, null, 2));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Integration Steps
|
|
145
|
+
|
|
146
|
+
For each paginating MCP server:
|
|
147
|
+
|
|
148
|
+
1. **Add dependency:**
|
|
149
|
+
```bash
|
|
150
|
+
bun add ../../large-output
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
2. **Remove pagination params from tools:**
|
|
154
|
+
- Remove `limit`, `page`, `offset` parameters
|
|
155
|
+
- Or make them internal defaults, not user-facing
|
|
156
|
+
|
|
157
|
+
3. **Fetch all data:**
|
|
158
|
+
- Remove pagination loops
|
|
159
|
+
- Fetch maximum amount in single call
|
|
160
|
+
|
|
161
|
+
4. **Use `handleMCPOutput`:**
|
|
162
|
+
```typescript
|
|
163
|
+
return handleMCPOutput(JSON.stringify(results));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/large-output
|
|
3
|
+
*
|
|
4
|
+
* Shared utility for handling large MCP outputs with automatic file fallback.
|
|
5
|
+
*
|
|
6
|
+
* When output size exceeds the threshold, automatically writes to a temp file
|
|
7
|
+
* and returns a structured response with file path and preview.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration options for the output handler
|
|
16
|
+
*/
|
|
17
|
+
export interface LargeOutputOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Character threshold for file fallback
|
|
20
|
+
* @default 15000 (safety margin under Claude's ~20K limit)
|
|
21
|
+
*/
|
|
22
|
+
threshold?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Number of characters to include in preview when writing to file
|
|
26
|
+
* @default 500
|
|
27
|
+
*/
|
|
28
|
+
previewLength?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Custom temp directory for output files
|
|
32
|
+
* @default OS temp directory
|
|
33
|
+
*/
|
|
34
|
+
tempDir?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom filename prefix
|
|
38
|
+
* @default "mcp_output"
|
|
39
|
+
*/
|
|
40
|
+
filenamePrefix?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether to include a size indicator in the response
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
includeSize?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether to include a preview in the file response
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
includePreview?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Response when output is returned inline (under threshold)
|
|
57
|
+
*/
|
|
58
|
+
export interface InlineOutputResponse {
|
|
59
|
+
type: "inline";
|
|
60
|
+
content: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Response when output is written to file (over threshold)
|
|
65
|
+
*/
|
|
66
|
+
export interface FileOutputResponse {
|
|
67
|
+
type: "file";
|
|
68
|
+
path: string;
|
|
69
|
+
size: number;
|
|
70
|
+
preview?: string;
|
|
71
|
+
sizeFormatted?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Union type for all possible responses
|
|
76
|
+
*/
|
|
77
|
+
export type OutputResponse = InlineOutputResponse | FileOutputResponse;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Default options
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_OPTIONS: Required<Omit<LargeOutputOptions, "tempDir" | "filenamePrefix">> = {
|
|
83
|
+
threshold: 15000,
|
|
84
|
+
previewLength: 500,
|
|
85
|
+
includeSize: true,
|
|
86
|
+
includePreview: true,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format bytes to human-readable string
|
|
91
|
+
*/
|
|
92
|
+
function formatSize(bytes: number): string {
|
|
93
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
94
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
95
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate a unique filename with timestamp
|
|
100
|
+
*/
|
|
101
|
+
function generateFilename(prefix: string): string {
|
|
102
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
|
|
103
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
104
|
+
return `${prefix}_${timestamp}_${random}.txt`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure directory exists, create if not
|
|
109
|
+
*/
|
|
110
|
+
function ensureDir(dir: string): void {
|
|
111
|
+
if (!fs.existsSync(dir)) {
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle output with automatic file fallback
|
|
118
|
+
*
|
|
119
|
+
* @param content - The content to return
|
|
120
|
+
* @param options - Configuration options
|
|
121
|
+
* @returns Structured response with inline content or file reference
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { handleOutput } from "@mcp/large-output";
|
|
126
|
+
*
|
|
127
|
+
* const results = await fetchLargeData();
|
|
128
|
+
* const response = handleOutput(JSON.stringify(results));
|
|
129
|
+
*
|
|
130
|
+
* if (response.type === "file") {
|
|
131
|
+
* console.log(`Written to: ${response.path}`);
|
|
132
|
+
* } else {
|
|
133
|
+
* console.log(response.content);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function handleOutput(
|
|
138
|
+
content: string,
|
|
139
|
+
options: LargeOutputOptions = {}
|
|
140
|
+
): OutputResponse {
|
|
141
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
142
|
+
const contentLength = content.length;
|
|
143
|
+
|
|
144
|
+
// Return inline if under threshold
|
|
145
|
+
if (contentLength <= opts.threshold) {
|
|
146
|
+
return {
|
|
147
|
+
type: "inline",
|
|
148
|
+
content,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Write to file if over threshold
|
|
153
|
+
const tempDir = options.tempDir || os.tmpdir();
|
|
154
|
+
const filenamePrefix = options.filenamePrefix || "mcp_output";
|
|
155
|
+
ensureDir(tempDir);
|
|
156
|
+
|
|
157
|
+
const filename = generateFilename(filenamePrefix);
|
|
158
|
+
const filepath = path.join(tempDir, filename);
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(filepath, content, "utf-8");
|
|
161
|
+
|
|
162
|
+
const response: FileOutputResponse = {
|
|
163
|
+
type: "file",
|
|
164
|
+
path: filepath,
|
|
165
|
+
size: contentLength,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (opts.includeSize) {
|
|
169
|
+
response.sizeFormatted = formatSize(contentLength);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (opts.includePreview) {
|
|
173
|
+
response.preview = content.slice(0, opts.previewLength);
|
|
174
|
+
if (contentLength > opts.previewLength) {
|
|
175
|
+
response.preview += "...";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return response;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Convert an OutputResponse to a JSON string for MCP tool return
|
|
184
|
+
*
|
|
185
|
+
* @param response - The response from handleOutput()
|
|
186
|
+
* @returns JSON string suitable for MCP tool return value
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const response = handleOutput(largeContent);
|
|
191
|
+
* return JSON.stringify(toMCPResponse(response));
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function toMCPResponse(response: OutputResponse): string {
|
|
195
|
+
if (response.type === "inline") {
|
|
196
|
+
return response.content;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// For file type, return structured JSON
|
|
200
|
+
const result: Record<string, unknown> = {
|
|
201
|
+
type: "file",
|
|
202
|
+
path: response.path,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (response.size !== undefined) {
|
|
206
|
+
result.size = response.size;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (response.sizeFormatted !== undefined) {
|
|
210
|
+
result.sizeFormatted = response.sizeFormatted;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (response.preview !== undefined) {
|
|
214
|
+
result.preview = response.preview;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return JSON.stringify(result, null, 2);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Convenience function: handle output and return MCP-formatted string
|
|
222
|
+
*
|
|
223
|
+
* @param content - The content to return
|
|
224
|
+
* @param options - Configuration options
|
|
225
|
+
* @returns JSON string or content directly
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* import { handleMCPOutput } from "@mcp/large-output";
|
|
230
|
+
*
|
|
231
|
+
* // In your MCP tool handler:
|
|
232
|
+
* return handleMCPOutput(JSON.stringify(results));
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function handleMCPOutput(
|
|
236
|
+
content: string,
|
|
237
|
+
options: LargeOutputOptions = {}
|
|
238
|
+
): string {
|
|
239
|
+
const response = handleOutput(content, options);
|
|
240
|
+
return toMCPResponse(response);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Batch handler for multiple outputs
|
|
245
|
+
*
|
|
246
|
+
* @param contents - Array of content strings
|
|
247
|
+
* @param options - Configuration options (applied to all)
|
|
248
|
+
* @returns Array of OutputResponse
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* const outputs = handleBatch([data1, data2, data3]);
|
|
253
|
+
* // Each output handled independently
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export function handleBatch(
|
|
257
|
+
contents: string[],
|
|
258
|
+
options: LargeOutputOptions = {}
|
|
259
|
+
): OutputResponse[] {
|
|
260
|
+
return contents.map((content) => handleOutput(content, options));
|
|
261
|
+
}
|
|
262
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/large-output",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared utility for handling large MCP outputs with automatic file fallback",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.ts",
|
|
10
|
+
"import": "./index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"large-output",
|
|
17
|
+
"file-fallback"
|
|
18
|
+
],
|
|
19
|
+
"author": "Ebowwa Labs <labs@ebowwa.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://github.com/ebowwa/codespaces/tree/main/packages/src/large-output#readme",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/ebowwa/codespaces.git",
|
|
25
|
+
"directory": "packages/src/large-output"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/ebowwa/codespaces/issues"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|