@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.
Files changed (3) hide show
  1. package/README.md +168 -0
  2. package/index.ts +262 -0
  3. 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
+ }