@pripanggalih/clickup-mcp 1.6.1

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 (57) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +295 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +184 -0
  6. package/dist/clickup-text.d.ts +83 -0
  7. package/dist/clickup-text.d.ts.map +1 -0
  8. package/dist/clickup-text.js +563 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +135 -0
  12. package/dist/resources/space-resources.d.ts +6 -0
  13. package/dist/resources/space-resources.d.ts.map +1 -0
  14. package/dist/resources/space-resources.js +95 -0
  15. package/dist/shared/config.d.ts +11 -0
  16. package/dist/shared/config.d.ts.map +1 -0
  17. package/dist/shared/config.js +61 -0
  18. package/dist/shared/data-uri.d.ts +14 -0
  19. package/dist/shared/data-uri.d.ts.map +1 -0
  20. package/dist/shared/data-uri.js +34 -0
  21. package/dist/shared/image-processing.d.ts +13 -0
  22. package/dist/shared/image-processing.d.ts.map +1 -0
  23. package/dist/shared/image-processing.js +199 -0
  24. package/dist/shared/types.d.ts +21 -0
  25. package/dist/shared/types.d.ts.map +1 -0
  26. package/dist/shared/types.js +2 -0
  27. package/dist/shared/utils.d.ts +71 -0
  28. package/dist/shared/utils.d.ts.map +1 -0
  29. package/dist/shared/utils.js +508 -0
  30. package/dist/test-utils.d.ts +23 -0
  31. package/dist/test-utils.d.ts.map +1 -0
  32. package/dist/test-utils.js +44 -0
  33. package/dist/tools/admin-tools.d.ts +3 -0
  34. package/dist/tools/admin-tools.d.ts.map +1 -0
  35. package/dist/tools/admin-tools.js +288 -0
  36. package/dist/tools/doc-tools.d.ts +4 -0
  37. package/dist/tools/doc-tools.d.ts.map +1 -0
  38. package/dist/tools/doc-tools.js +436 -0
  39. package/dist/tools/list-tools.d.ts +4 -0
  40. package/dist/tools/list-tools.d.ts.map +1 -0
  41. package/dist/tools/list-tools.js +175 -0
  42. package/dist/tools/search-tools.d.ts +3 -0
  43. package/dist/tools/search-tools.d.ts.map +1 -0
  44. package/dist/tools/search-tools.js +161 -0
  45. package/dist/tools/space-tools.d.ts +3 -0
  46. package/dist/tools/space-tools.d.ts.map +1 -0
  47. package/dist/tools/space-tools.js +128 -0
  48. package/dist/tools/task-tools.d.ts +8 -0
  49. package/dist/tools/task-tools.d.ts.map +1 -0
  50. package/dist/tools/task-tools.js +329 -0
  51. package/dist/tools/task-write-tools.d.ts +3 -0
  52. package/dist/tools/task-write-tools.d.ts.map +1 -0
  53. package/dist/tools/task-write-tools.js +567 -0
  54. package/dist/tools/time-tools.d.ts +4 -0
  55. package/dist/tools/time-tools.d.ts.map +1 -0
  56. package/dist/tools/time-tools.js +338 -0
  57. package/package.json +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.serverPromise = void 0;
5
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ const config_1 = require("./shared/config");
8
+ const utils_1 = require("./shared/utils");
9
+ // Import tool registration functions
10
+ const task_tools_1 = require("./tools/task-tools");
11
+ const task_write_tools_1 = require("./tools/task-write-tools");
12
+ const search_tools_1 = require("./tools/search-tools");
13
+ const space_tools_1 = require("./tools/space-tools");
14
+ const list_tools_1 = require("./tools/list-tools");
15
+ const time_tools_1 = require("./tools/time-tools");
16
+ const doc_tools_1 = require("./tools/doc-tools");
17
+ const admin_tools_1 = require("./tools/admin-tools");
18
+ const space_resources_1 = require("./resources/space-resources");
19
+ // Create server variable that will be initialized later
20
+ let server;
21
+ // Register tools based on mode with user data for enhanced documentation
22
+ async function initializeServer() {
23
+ console.error(`Starting ClickUp MCP in ${config_1.CONFIG.mode} mode`);
24
+ // Fetch current user and spaces for enhanced tool documentation and API health check
25
+ const [userData, spacesIndex] = await Promise.all([
26
+ (0, utils_1.getCurrentUser)(),
27
+ (0, utils_1.getSpaceSearchIndex)()
28
+ ]);
29
+ const spaces = spacesIndex._docs || [];
30
+ console.error(`Connected as: ${userData.user.username} (${userData.user.email})`);
31
+ // Filter out archived spaces and format as simple list
32
+ const activeSpaces = spaces.filter((s) => !s.archived);
33
+ const formattedSpaces = activeSpaces
34
+ .map((s) => `- ${s.name} (space_id: ${s.id})`)
35
+ .join('\n');
36
+ const instructions = [
37
+ `ClickUp is a Ticket system. It is used to track tasks, bugs, and other work items.`,
38
+ `Is you are asked for infos about projects or tasks, search for tasks or documents in ClickUp (this MCP) first.`,
39
+ `The following spaces/projects are available:`,
40
+ formattedSpaces
41
+ ].join('\n');
42
+ console.error(`Pre-loaded ${activeSpaces.length} active spaces`);
43
+ // Create the MCP server with instructions
44
+ server = new mcp_js_1.McpServer({
45
+ name: "Clickup MCP",
46
+ version: require('../package.json').version,
47
+ }, {
48
+ instructions
49
+ });
50
+ // Register prompts
51
+ const lang = config_1.CONFIG.primaryLanguageHint === 'de' ? 'de' : 'en';
52
+ // Register "my-todos" prompt
53
+ server.registerPrompt("my-todos", {
54
+ title: lang === 'de' ? "Meine TODOs" : "My TODOs",
55
+ description: lang === 'de'
56
+ ? "Meine aktuellen TODO-Aufgaben aus ClickUp abrufen und nach Priorität kategorisiert analysieren"
57
+ : "Get and analyze my current TODO tasks from ClickUp, categorized by priority"
58
+ }, () => {
59
+ const messages = [{
60
+ role: "user",
61
+ content: {
62
+ type: "text",
63
+ text: lang === 'de'
64
+ ? `Kannst du in ClickUp nachsehen, was meine aktuellen TODOs sind? Bitte suche nach allen offenen Aufgaben, die mir zugewiesen sind, analysiere deren Inhalt und kategorisiere sie nach erkennbarer Priorität (dringend, hoch, normal, niedrig). Für jede Kategorie gib eine kurze Zusammenfassung dessen, was getan werden muss und hebe Fälligkeitstermine oder wichtige Details aus den Aufgabenbeschreibungen hervor.
65
+
66
+ Bitte strukturiere deine Antwort mit:
67
+ 1. **Zusammenfassung**: Gesamtanzahl der Aufgaben und allgemeine Prioritätsverteilung
68
+ 2. **Dringende Aufgaben**: Aufgaben, die sofortige Aufmerksamkeit benötigen (heute fällig, überfällig oder als dringend markiert)
69
+ 3. **Hohe Priorität**: Wichtige Aufgaben, die bald erledigt werden sollten
70
+ 4. **Normale Priorität**: Regelmäßige Aufgaben, die später geplant werden können
71
+ 5. **Niedrige Priorität**: Aufgaben, die erledigt werden können, wenn Zeit vorhanden ist
72
+ 6. **Empfehlungen**: Vorgeschlagene Maßnahmen oder Prioritäten für den kommenden Zeitraum
73
+
74
+ Verwende die ClickUp-Suchtools, um mir zugewiesene Aufgaben zu finden, und hole detaillierte Informationen über die wichtigsten mit getTaskById.`
75
+ : `Can you look into ClickUp and check what my current TODO's are? Please search for all open tasks assigned to me, analyze their content, and categorize them by apparent priority (urgent, high, normal, low). For each category, provide a brief summary of what needs to be done and highlight any due dates or important details from the task descriptions.
76
+
77
+ Please structure your response with:
78
+ 1. **Summary**: Total number of tasks and overall priority distribution
79
+ 2. **Urgent Tasks**: Tasks that need immediate attention (due today, overdue, or marked as urgent)
80
+ 3. **High Priority**: Important tasks that should be addressed soon
81
+ 4. **Normal Priority**: Regular tasks that can be scheduled for later
82
+ 5. **Low Priority**: Tasks that can be done when time permits
83
+ 6. **Recommendations**: Suggested actions or priorities for the upcoming period
84
+
85
+ Use the ClickUp search tools to find tasks assigned to me, and get detailed information about the most important ones using getTaskById.`
86
+ }
87
+ }];
88
+ return { messages };
89
+ });
90
+ if (config_1.CONFIG.mode === 'read-minimal') {
91
+ // Core task context tools for AI coding assistance
92
+ // Only getTaskById and searchTasks
93
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
94
+ (0, search_tools_1.registerSearchTools)(server, userData);
95
+ }
96
+ else if (config_1.CONFIG.mode === 'read') {
97
+ // All read-only tools
98
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
99
+ (0, search_tools_1.registerSearchTools)(server, userData);
100
+ (0, space_tools_1.registerSpaceTools)(server);
101
+ (0, space_resources_1.registerSpaceResources)(server);
102
+ (0, list_tools_1.registerListToolsRead)(server);
103
+ (0, time_tools_1.registerTimeToolsRead)(server);
104
+ (0, doc_tools_1.registerDocumentToolsRead)(server);
105
+ }
106
+ else if (config_1.CONFIG.mode === 'write') {
107
+ // All tools (full functionality)
108
+ (0, task_tools_1.registerTaskToolsRead)(server, userData);
109
+ (0, task_write_tools_1.registerTaskToolsWrite)(server, userData);
110
+ (0, search_tools_1.registerSearchTools)(server, userData);
111
+ (0, space_tools_1.registerSpaceTools)(server);
112
+ (0, space_resources_1.registerSpaceResources)(server);
113
+ (0, list_tools_1.registerListToolsRead)(server);
114
+ (0, list_tools_1.registerListToolsWrite)(server);
115
+ (0, time_tools_1.registerTimeToolsRead)(server);
116
+ (0, time_tools_1.registerTimeToolsWrite)(server);
117
+ (0, doc_tools_1.registerDocumentToolsRead)(server);
118
+ (0, doc_tools_1.registerDocumentToolsWrite)(server);
119
+ (0, admin_tools_1.registerAdminToolsWrite)(server);
120
+ }
121
+ return server;
122
+ }
123
+ // Initialize server with enhanced documentation and export
124
+ const serverPromise = initializeServer();
125
+ exports.serverPromise = serverPromise;
126
+ // Only connect to the transport if this file is being run directly (not imported)
127
+ // OR if not being imported by CLI (to support Claude Desktop's module loading)
128
+ const isCliMode = process.argv.some(arg => arg.includes('cli.ts') || arg.includes('cli.js'));
129
+ if (require.main === module || !isCliMode) {
130
+ // Start receiving messages on stdin and sending messages on stdout after initialization
131
+ serverPromise.then(() => {
132
+ const transport = new stdio_js_1.StdioServerTransport();
133
+ server.connect(transport);
134
+ }).catch(console.error);
135
+ }
@@ -0,0 +1,6 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register ClickUp space resources using resource templates for dynamic discovery
4
+ */
5
+ export declare function registerSpaceResources(server: McpServer): void;
6
+ //# sourceMappingURL=space-resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"space-resources.d.ts","sourceRoot":"","sources":["../../src/resources/space-resources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAiBtF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAwFvD"}
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSpaceResources = registerSpaceResources;
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const utils_1 = require("../shared/utils");
6
+ /**
7
+ * Extract space ID from clickup:// URI
8
+ */
9
+ function extractSpaceIdFromUri(uriString) {
10
+ try {
11
+ const url = new URL(uriString);
12
+ const pathParts = url.pathname.split('/');
13
+ return pathParts[pathParts.length - 1];
14
+ }
15
+ catch (error) {
16
+ throw new Error(`Invalid ClickUp space URI: ${uriString}`);
17
+ }
18
+ }
19
+ /**
20
+ * Register ClickUp space resources using resource templates for dynamic discovery
21
+ */
22
+ function registerSpaceResources(server) {
23
+ // Create resource template for ClickUp spaces
24
+ const spaceTemplate = new mcp_js_1.ResourceTemplate("clickup://space/{spaceId}", {
25
+ list: async () => {
26
+ try {
27
+ const searchIndex = await (0, utils_1.getSpaceSearchIndex)();
28
+ if (!searchIndex) {
29
+ return { resources: [] };
30
+ }
31
+ const spaces = searchIndex._docs || [];
32
+ // Filter out archived spaces for resource listing
33
+ const activeSpaces = spaces.filter((space) => !space.archived);
34
+ return {
35
+ resources: activeSpaces.map((space) => ({
36
+ uri: `clickup://space/${space.id}`,
37
+ name: `${space.name} ClickUp Space.txt`,
38
+ title: `${space.name} ClickUp Space`,
39
+ mimeType: "text/plain"
40
+ }))
41
+ };
42
+ }
43
+ catch (error) {
44
+ console.error("Error listing space resources:", error);
45
+ return { resources: [] };
46
+ }
47
+ }
48
+ });
49
+ // Register resource template for ClickUp spaces
50
+ server.registerResource("clickup-spaces", spaceTemplate, {
51
+ title: "ClickUp Spaces",
52
+ description: "Access ClickUp spaces with their complete structure including lists, folders, and documents",
53
+ }, async (uri) => {
54
+ try {
55
+ const spaceId = extractSpaceIdFromUri(uri.toString());
56
+ // Fetch space content including lists, folders, and documents
57
+ const { lists, folders, documents } = await (0, utils_1.getSpaceContent)(spaceId);
58
+ // Get space details from the search index
59
+ const searchIndex = await (0, utils_1.getSpaceSearchIndex)();
60
+ const spaces = searchIndex._docs || [];
61
+ const space = spaces.find((s) => s.id === spaceId);
62
+ if (!space) {
63
+ return {
64
+ contents: [{
65
+ uri: uri.toString(),
66
+ text: `Space with ID ${spaceId} not found.`,
67
+ }]
68
+ };
69
+ }
70
+ // Format the content using the shared tree formatting function
71
+ const treeContent = (0, utils_1.formatSpaceTree)(space, lists, folders, documents);
72
+ // Add resource metadata
73
+ const metadata = [
74
+ '\n---',
75
+ `ℹ️ Resource last updated: ${new Date().toISOString()}`,
76
+ `💡 For real-time data, use the searchSpaces tool`
77
+ ].join('\n');
78
+ return {
79
+ contents: [{
80
+ uri: uri.toString(),
81
+ text: treeContent + metadata,
82
+ }]
83
+ };
84
+ }
85
+ catch (error) {
86
+ console.error("Error reading space resource:", error);
87
+ return {
88
+ contents: [{
89
+ uri: uri.toString(),
90
+ text: `Error reading space: ${error instanceof Error ? error.message : 'Unknown error'}`,
91
+ }]
92
+ };
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,11 @@
1
+ export declare const rawPrimaryLang: string | undefined;
2
+ export type McpMode = 'read-minimal' | 'read' | 'write';
3
+ export declare const CONFIG: {
4
+ apiKey: string;
5
+ teamId: string;
6
+ maxImages: number;
7
+ maxResponseSizeMB: number;
8
+ primaryLanguageHint: string | undefined;
9
+ mode: McpMode;
10
+ };
11
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG = exports.rawPrimaryLang = void 0;
4
+ exports.rawPrimaryLang = process.env.CLICKUP_PRIMARY_LANGUAGE || process.env.LANG;
5
+ let detectedLanguageHint = undefined;
6
+ /**
7
+ * Enhanced language detection that handles various formats and common language names
8
+ */
9
+ function detectLanguage(rawLang) {
10
+ if (!rawLang)
11
+ return undefined;
12
+ const normalizedLang = rawLang.toLowerCase().trim();
13
+ // German language detection
14
+ if (normalizedLang === 'de' || normalizedLang === 'german' || normalizedLang === 'deutsch' || normalizedLang.startsWith('de_') || normalizedLang.startsWith('de-')) {
15
+ return 'de';
16
+ }
17
+ // English language detection
18
+ if (normalizedLang === 'en' || normalizedLang === 'english' || normalizedLang.startsWith('en_') || normalizedLang.startsWith('en-')) {
19
+ return 'en';
20
+ }
21
+ // French language detection
22
+ if (normalizedLang === 'fr' || normalizedLang === 'french' || normalizedLang === 'français' || normalizedLang.startsWith('fr_') || normalizedLang.startsWith('fr-')) {
23
+ return 'fr';
24
+ }
25
+ // Spanish language detection
26
+ if (normalizedLang === 'es' || normalizedLang === 'spanish' || normalizedLang === 'español' || normalizedLang.startsWith('es_') || normalizedLang.startsWith('es-')) {
27
+ return 'es';
28
+ }
29
+ // Italian language detection
30
+ if (normalizedLang === 'it' || normalizedLang === 'italian' || normalizedLang === 'italiano' || normalizedLang.startsWith('it_') || normalizedLang.startsWith('it-')) {
31
+ return 'it';
32
+ }
33
+ // Fallback: extract the primary language part (e.g., 'en' from 'en_US.UTF-8' or 'en-GB')
34
+ const langPart = normalizedLang.match(/^[a-zA-Z]{2,3}/);
35
+ if (langPart) {
36
+ return langPart[0].toLowerCase();
37
+ }
38
+ return undefined;
39
+ }
40
+ if (exports.rawPrimaryLang) {
41
+ detectedLanguageHint = detectLanguage(exports.rawPrimaryLang);
42
+ }
43
+ const rawMode = process.env.CLICKUP_MCP_MODE?.toLowerCase();
44
+ let mcpMode = 'write'; // Default to write (full functionality)
45
+ if (rawMode === 'read-minimal' || rawMode === 'read') {
46
+ mcpMode = rawMode;
47
+ }
48
+ else if (rawMode && rawMode !== 'write') {
49
+ console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
50
+ }
51
+ exports.CONFIG = {
52
+ apiKey: process.env.CLICKUP_API_KEY,
53
+ teamId: process.env.CLICKUP_TEAM_ID,
54
+ maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
55
+ maxResponseSizeMB: process.env.MAX_RESPONSE_SIZE_MB ? parseFloat(process.env.MAX_RESPONSE_SIZE_MB) : 1,
56
+ primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
57
+ mode: mcpMode,
58
+ };
59
+ if (!exports.CONFIG.apiKey || !exports.CONFIG.teamId) {
60
+ throw new Error("Missing Clickup API key or team ID");
61
+ }
@@ -0,0 +1,14 @@
1
+ export interface ParsedDataUri {
2
+ mimeType: string;
3
+ base64Data: string;
4
+ }
5
+ /**
6
+ * Parse data URI strings of the form data:mime/type;base64,....
7
+ * Returns null if the value is not a base64 data URI.
8
+ */
9
+ export declare function parseDataUri(dataUri: string): ParsedDataUri | null;
10
+ /**
11
+ * Estimate the decoded byte-size of base64 data without allocating buffers
12
+ */
13
+ export declare function estimateBase64Size(base64Data: string): number;
14
+ //# sourceMappingURL=data-uri.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-uri.d.ts","sourceRoot":"","sources":["../../src/shared/data-uri.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseDataUri = parseDataUri;
4
+ exports.estimateBase64Size = estimateBase64Size;
5
+ /**
6
+ * Parse data URI strings of the form data:mime/type;base64,....
7
+ * Returns null if the value is not a base64 data URI.
8
+ */
9
+ function parseDataUri(dataUri) {
10
+ if (!dataUri.startsWith("data:")) {
11
+ return null;
12
+ }
13
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/s);
14
+ if (!match) {
15
+ return null;
16
+ }
17
+ const [, mimeType, base64Part] = match;
18
+ const sanitizedBase64 = base64Part.replace(/\s+/g, "");
19
+ if (!sanitizedBase64) {
20
+ return null;
21
+ }
22
+ return {
23
+ mimeType,
24
+ base64Data: sanitizedBase64,
25
+ };
26
+ }
27
+ /**
28
+ * Estimate the decoded byte-size of base64 data without allocating buffers
29
+ */
30
+ function estimateBase64Size(base64Data) {
31
+ const sanitized = base64Data.replace(/\s+/g, "");
32
+ const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
33
+ return Math.max(0, (sanitized.length * 3) / 4 - padding);
34
+ }
@@ -0,0 +1,13 @@
1
+ import { ContentBlock, ImageMetadataBlock } from "./types";
2
+ /**
3
+ * Downloads images from image_metadata blocks and applies smart size/count limiting
4
+ * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
5
+ * Uses intelligent size calculation accounting for text content
6
+ *
7
+ * @param content Array of content blocks that may contain image_metadata blocks
8
+ * @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
9
+ * @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
10
+ * @returns Promise resolving to content array with downloaded images or placeholders
11
+ */
12
+ export declare function downloadImages(content: (ContentBlock | ImageMetadataBlock)[], maxImages?: number, maxSizeMB?: number): Promise<ContentBlock[]>;
13
+ //# sourceMappingURL=image-processing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-processing.d.ts","sourceRoot":"","sources":["../../src/shared/image-processing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAmDzD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,EAAE,SAAS,GAAE,MAAyB,EAAE,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BhM"}
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadImages = downloadImages;
4
+ const config_1 = require("./config");
5
+ const data_uri_1 = require("./data-uri");
6
+ const buffer_1 = require("buffer");
7
+ /**
8
+ * Detect MIME type from image binary data using magic bytes (file signatures)
9
+ * Returns null if the format is not recognized
10
+ */
11
+ function detectMimeTypeFromBuffer(buffer) {
12
+ const bytes = new Uint8Array(buffer);
13
+ if (bytes.length < 12)
14
+ return null;
15
+ // PNG: 89 50 4E 47 (‰PNG)
16
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
17
+ return "image/png";
18
+ }
19
+ // JPEG: FF D8 FF
20
+ if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
21
+ return "image/jpeg";
22
+ }
23
+ // GIF: 47 49 46 38 (GIF8)
24
+ if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) {
25
+ return "image/gif";
26
+ }
27
+ // WebP: RIFF....WEBP
28
+ if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
29
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
30
+ return "image/webp";
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Detect MIME type from base64-encoded image data
36
+ * Decodes just enough bytes to check magic numbers
37
+ */
38
+ function detectMimeTypeFromBase64(base64Data) {
39
+ // Need at least 16 base64 chars to decode 12 bytes for magic number detection
40
+ if (base64Data.length < 16)
41
+ return null;
42
+ // Create a Uint8Array directly from the decoded bytes to avoid Buffer pooling issues
43
+ const header = buffer_1.Buffer.from(base64Data.slice(0, 16), "base64");
44
+ const bytes = new Uint8Array(header);
45
+ return detectMimeTypeFromBuffer(bytes.buffer);
46
+ }
47
+ /**
48
+ * Downloads images from image_metadata blocks and applies smart size/count limiting
49
+ * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
50
+ * Uses intelligent size calculation accounting for text content
51
+ *
52
+ * @param content Array of content blocks that may contain image_metadata blocks
53
+ * @param maxImages Maximum number of images to keep (defaults to CONFIG.maxImages)
54
+ * @param maxSizeMB Maximum response size in MB (defaults to CONFIG.maxResponseSizeMB)
55
+ * @returns Promise resolving to content array with downloaded images or placeholders
56
+ */
57
+ async function downloadImages(content, maxImages = config_1.CONFIG.maxImages, maxSizeMB = config_1.CONFIG.maxResponseSizeMB) {
58
+ // First apply count-based limiting to image_metadata blocks
59
+ const countLimitedContent = applyCountBasedLimitToImageMetadata(content, maxImages);
60
+ // Calculate text size to determine available image budget
61
+ const textContent = countLimitedContent.filter(block => block.type !== "image_metadata");
62
+ const textSizeBytes = JSON.stringify(textContent).length;
63
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
64
+ const availableImageBudget = Math.max(0, maxSizeBytes - textSizeBytes);
65
+ // Calculate per-image budget
66
+ const imageMetadataBlocks = countLimitedContent.filter(block => block.type === "image_metadata");
67
+ const perImageBudget = imageMetadataBlocks.length > 0 ? availableImageBudget / imageMetadataBlocks.length : 0;
68
+ // Download images in parallel and replace image_metadata blocks
69
+ const downloadPromises = countLimitedContent.map(async (block) => {
70
+ if (block.type === "image_metadata") {
71
+ if (block.inlineData) {
72
+ return convertInlineImage(block, perImageBudget);
73
+ }
74
+ return await downloadSingleImage(block, perImageBudget);
75
+ }
76
+ return block;
77
+ });
78
+ return Promise.all(downloadPromises);
79
+ }
80
+ /**
81
+ * Apply count-based limiting to image_metadata blocks
82
+ * Keeps the most recent image_metadata blocks (newest last)
83
+ */
84
+ function applyCountBasedLimitToImageMetadata(content, maxImages) {
85
+ // Find all image_metadata block indices
86
+ const imageMetadataIndices = [];
87
+ content.forEach((block, index) => {
88
+ if (block.type === "image_metadata") {
89
+ imageMetadataIndices.push(index);
90
+ }
91
+ });
92
+ // If we have fewer images than the limit, return the original content
93
+ if (imageMetadataIndices.length <= maxImages) {
94
+ return content;
95
+ }
96
+ // Determine which image_metadata blocks to remove (keep the most recent ones)
97
+ const imagesToRemove = imageMetadataIndices.slice(0, imageMetadataIndices.length - maxImages);
98
+ // Create a new content array with excess image_metadata blocks replaced by text placeholders
99
+ return content.map((block, index) => {
100
+ if (block.type === "image_metadata" && imagesToRemove.includes(index)) {
101
+ return {
102
+ type: "text",
103
+ text: "[Image removed due to count limitations. Only the most recent images are shown.]",
104
+ };
105
+ }
106
+ return block;
107
+ });
108
+ }
109
+ /**
110
+ * Download a single image from an image_metadata block, trying different sizes if needed
111
+ */
112
+ async function downloadSingleImage(imageMetadata, perImageBudget) {
113
+ const fallbackText = createImageFallback(imageMetadata);
114
+ // Try each URL in order (largest to smallest)
115
+ for (const url of imageMetadata.urls) {
116
+ const abortController = new AbortController();
117
+ try {
118
+ const response = await fetch(url, {
119
+ signal: abortController.signal
120
+ });
121
+ if (!response.ok) {
122
+ console.error(`Failed to fetch image from ${url}: ${response.status}`);
123
+ continue;
124
+ }
125
+ // Check Content-Length header first to avoid downloading large images
126
+ const contentLength = response.headers.get("Content-Length");
127
+ if (contentLength) {
128
+ const imageSizeBytes = parseInt(contentLength, 10);
129
+ if (imageSizeBytes > perImageBudget) {
130
+ // Cancel the request to stop any ongoing download
131
+ abortController.abort();
132
+ console.error(`Image from ${url} is ${imageSizeBytes} bytes (from Content-Length), exceeds budget of ${perImageBudget} bytes`);
133
+ // Continue to try smaller thumbnail
134
+ continue;
135
+ }
136
+ }
137
+ const imageBuffer = await response.arrayBuffer();
138
+ const actualSizeBytes = imageBuffer.byteLength;
139
+ // Double-check actual size (in case Content-Length was missing or incorrect)
140
+ if (actualSizeBytes <= perImageBudget) {
141
+ // Detect actual MIME type from binary data, fall back to header or default
142
+ const detectedMimeType = detectMimeTypeFromBuffer(imageBuffer);
143
+ const mimeType = detectedMimeType || response.headers.get("Content-Type") || "image/png";
144
+ return {
145
+ type: "image",
146
+ mimeType,
147
+ data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
148
+ };
149
+ }
150
+ else {
151
+ // Cancel to clean up the connection
152
+ abortController.abort();
153
+ console.error(`Image from ${url} is ${actualSizeBytes} bytes (actual size), exceeds budget of ${perImageBudget} bytes`);
154
+ // Continue to try smaller thumbnail
155
+ }
156
+ }
157
+ catch (error) {
158
+ if (error.name === 'AbortError') {
159
+ // Request was cancelled, this is expected - continue to next URL
160
+ continue;
161
+ }
162
+ console.error(`Error fetching image from ${url}: ${error.message || "Unknown error"}`);
163
+ // Continue to try next URL
164
+ }
165
+ }
166
+ // If all URLs failed or were too large, return fallback text
167
+ return fallbackText;
168
+ }
169
+ /**
170
+ * Create a fallback text block when an image cannot be included
171
+ */
172
+ function createImageFallback(imageMetadata) {
173
+ return {
174
+ type: "text",
175
+ text: `[Image "${imageMetadata.alt}" removed due to size limitations.]`,
176
+ };
177
+ }
178
+ /**
179
+ * Convert inline image data URIs into image blocks while respecting size budgets
180
+ */
181
+ function convertInlineImage(imageMetadata, perImageBudget) {
182
+ const inlineData = imageMetadata.inlineData;
183
+ if (!inlineData) {
184
+ return createImageFallback(imageMetadata);
185
+ }
186
+ const estimatedSize = (0, data_uri_1.estimateBase64Size)(inlineData.base64Data);
187
+ if (perImageBudget <= 0 || estimatedSize > perImageBudget) {
188
+ console.error(`Inline image for "${imageMetadata.alt}" is ${estimatedSize} bytes, exceeds budget of ${perImageBudget} bytes`);
189
+ return createImageFallback(imageMetadata);
190
+ }
191
+ // Detect actual MIME type from binary data, fall back to declared type or default
192
+ const detectedMimeType = detectMimeTypeFromBase64(inlineData.base64Data);
193
+ const mimeType = detectedMimeType || inlineData.mimeType || "image/png";
194
+ return {
195
+ type: "image",
196
+ mimeType,
197
+ data: inlineData.base64Data,
198
+ };
199
+ }
@@ -0,0 +1,21 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
2
+ import { ParsedDataUri } from "./data-uri";
3
+ export type ContentBlock = CallToolResult['content'][number];
4
+ export interface ImageMetadataBlock {
5
+ type: "image_metadata";
6
+ urls: string[];
7
+ alt: string;
8
+ inlineData?: ParsedDataUri;
9
+ }
10
+ export interface DatedContentEvent {
11
+ date: string;
12
+ contentBlocks: (ContentBlock | ImageMetadataBlock)[];
13
+ }
14
+ export interface Config {
15
+ apiKey: string;
16
+ teamId: string;
17
+ maxImages: number;
18
+ maxResponseSizeMB: number;
19
+ primaryLanguageHint: string | undefined;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAGD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,CAAC,YAAY,GAAG,kBAAkB,CAAC,EAAE,CAAC;CACtD;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });