@mindstone-engineering/mcp-server-gamma 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/dist/auth.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Gamma authentication module.
3
+ *
4
+ * Simple API key management — stored via env var (GAMMA_API_KEY)
5
+ * or configured at runtime via the configure_gamma_api_key tool.
6
+ *
7
+ * Auth: x-api-key header on all API requests.
8
+ */
9
+ /**
10
+ * Get the current API key.
11
+ */
12
+ export declare function getApiKey(): string;
13
+ /**
14
+ * Set the API key at runtime (from configure tool).
15
+ */
16
+ export declare function setApiKey(key: string): void;
17
+ /**
18
+ * Check if an API key is configured.
19
+ */
20
+ export declare function hasApiKey(): boolean;
21
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Gamma authentication module.
3
+ *
4
+ * Simple API key management — stored via env var (GAMMA_API_KEY)
5
+ * or configured at runtime via the configure_gamma_api_key tool.
6
+ *
7
+ * Auth: x-api-key header on all API requests.
8
+ */
9
+ /** Runtime API key — starts from env, can be updated via configure tool. */
10
+ let apiKey = process.env.GAMMA_API_KEY ?? '';
11
+ /**
12
+ * Get the current API key.
13
+ */
14
+ export function getApiKey() {
15
+ return apiKey;
16
+ }
17
+ /**
18
+ * Set the API key at runtime (from configure tool).
19
+ */
20
+ export function setApiKey(key) {
21
+ apiKey = key;
22
+ }
23
+ /**
24
+ * Check if an API key is configured.
25
+ */
26
+ export function hasApiKey() {
27
+ return apiKey.trim().length > 0;
28
+ }
29
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,46 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return {
40
+ success: false,
41
+ error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.`,
42
+ };
43
+ }
44
+ return response.json();
45
+ };
46
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gamma API HTTP client.
3
+ *
4
+ * Centralises x-api-key header injection, error handling, rate-limit
5
+ * messaging, and timeout handling for all Gamma API calls.
6
+ *
7
+ * Auth: x-api-key: {key}
8
+ * Base URL: https://public-api.gamma.app/v1.0
9
+ */
10
+ import { type GenerationRequest, type CreateFromTemplateRequest, type GenerationResponse, type GenerationStatus, type Theme, type Folder, type PaginatedResponse } from './types.js';
11
+ /**
12
+ * Create a new generation.
13
+ */
14
+ export declare function createGeneration(apiKey: string, request: GenerationRequest): Promise<GenerationResponse>;
15
+ /**
16
+ * Create from an existing template.
17
+ */
18
+ export declare function createFromTemplate(apiKey: string, request: CreateFromTemplateRequest): Promise<GenerationResponse>;
19
+ /**
20
+ * Get generation status (including export URLs when available).
21
+ */
22
+ export declare function getGenerationStatus(apiKey: string, generationId: string): Promise<GenerationStatus>;
23
+ /**
24
+ * List available themes.
25
+ */
26
+ export declare function listThemes(apiKey: string, options?: {
27
+ query?: string;
28
+ limit?: number;
29
+ after?: string;
30
+ }): Promise<PaginatedResponse<Theme>>;
31
+ /**
32
+ * List workspace folders.
33
+ */
34
+ export declare function listFolders(apiKey: string, options?: {
35
+ query?: string;
36
+ limit?: number;
37
+ after?: string;
38
+ }): Promise<PaginatedResponse<Folder>>;
39
+ /**
40
+ * Download an export file (PDF/PPTX) to the system tmpdir.
41
+ * Returns the absolute path of the downloaded file.
42
+ */
43
+ export declare function downloadExportFile(url: string, generationId: string, format: 'pdf' | 'pptx'): Promise<string>;
44
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Gamma API HTTP client.
3
+ *
4
+ * Centralises x-api-key header injection, error handling, rate-limit
5
+ * messaging, and timeout handling for all Gamma API calls.
6
+ *
7
+ * Auth: x-api-key: {key}
8
+ * Base URL: https://public-api.gamma.app/v1.0
9
+ */
10
+ import { GammaError, REQUEST_TIMEOUT_MS, } from './types.js';
11
+ const GAMMA_API_BASE = 'https://public-api.gamma.app/v1.0';
12
+ /**
13
+ * Make an authenticated request to the Gamma API.
14
+ */
15
+ async function gammaFetch(apiKey, endpoint, options = {}) {
16
+ const url = `${GAMMA_API_BASE}${endpoint}`;
17
+ console.error(`[Gamma API] ${options.method || 'GET'} ${url}`);
18
+ let response;
19
+ try {
20
+ response = await fetch(url, {
21
+ ...options,
22
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'x-api-key': apiKey,
26
+ ...options.headers,
27
+ },
28
+ });
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error && error.name === 'TimeoutError') {
32
+ throw new GammaError('Request to Gamma API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Gamma API is available.');
33
+ }
34
+ throw error;
35
+ }
36
+ // Handle rate limiting
37
+ if (response.status === 429) {
38
+ const retryAfter = response.headers.get('Retry-After');
39
+ const waitTime = retryAfter ? `${retryAfter} seconds` : 'a moment';
40
+ throw new GammaError(`Rate limited. Please wait ${waitTime} before retrying.`, 'RATE_LIMITED', `Wait ${waitTime} and try again.`);
41
+ }
42
+ // Handle auth errors
43
+ if (response.status === 401) {
44
+ throw new GammaError('Authentication failed', 'AUTH_FAILED', 'API key is invalid or revoked. Check your Gamma API key at https://gamma.app/settings/developers.');
45
+ }
46
+ if (response.status === 403) {
47
+ throw new GammaError('Access forbidden', 'AUTH_FAILED', 'Your API key does not have permission for this operation.');
48
+ }
49
+ // Handle not found
50
+ if (response.status === 404) {
51
+ throw new GammaError('Resource not found', 'NOT_FOUND', 'The requested resource does not exist. Check the ID and try again.');
52
+ }
53
+ // Handle other errors
54
+ if (!response.ok) {
55
+ const errorText = await response.text().catch(() => '');
56
+ console.error(`Gamma API error (${response.status}):`, errorText);
57
+ const statusMessage = response.status === 422
58
+ ? 'Validation error - check request parameters'
59
+ : response.status >= 500
60
+ ? 'Gamma server error - try again later'
61
+ : 'Request failed';
62
+ throw new GammaError(`Gamma API error (${response.status}): ${statusMessage}`, 'API_ERROR', 'Check the request parameters and try again.');
63
+ }
64
+ return response.json();
65
+ }
66
+ /**
67
+ * Create a new generation.
68
+ */
69
+ export async function createGeneration(apiKey, request) {
70
+ const body = {
71
+ inputText: request.inputText,
72
+ textMode: request.textMode || 'generate',
73
+ format: request.format || 'presentation',
74
+ };
75
+ if (request.themeId)
76
+ body.themeId = request.themeId;
77
+ if (request.numCards)
78
+ body.numCards = request.numCards;
79
+ if (request.cardSplit)
80
+ body.cardSplit = request.cardSplit;
81
+ if (request.additionalInstructions)
82
+ body.additionalInstructions = request.additionalInstructions;
83
+ if (request.folderIds)
84
+ body.folderIds = request.folderIds;
85
+ if (request.exportAs)
86
+ body.exportAs = request.exportAs;
87
+ if (request.textOptions)
88
+ body.textOptions = request.textOptions;
89
+ if (request.imageOptions)
90
+ body.imageOptions = request.imageOptions;
91
+ if (request.cardOptions)
92
+ body.cardOptions = request.cardOptions;
93
+ if (request.sharingOptions)
94
+ body.sharingOptions = request.sharingOptions;
95
+ return gammaFetch(apiKey, '/generations', {
96
+ method: 'POST',
97
+ body: JSON.stringify(body),
98
+ });
99
+ }
100
+ /**
101
+ * Create from an existing template.
102
+ */
103
+ export async function createFromTemplate(apiKey, request) {
104
+ const body = {
105
+ gammaId: request.gammaId,
106
+ };
107
+ if (request.prompt)
108
+ body.prompt = request.prompt;
109
+ if (request.themeId)
110
+ body.themeId = request.themeId;
111
+ if (request.folderIds)
112
+ body.folderIds = request.folderIds;
113
+ if (request.exportAs)
114
+ body.exportAs = request.exportAs;
115
+ if (request.imageOptions)
116
+ body.imageOptions = request.imageOptions;
117
+ if (request.sharingOptions)
118
+ body.sharingOptions = request.sharingOptions;
119
+ return gammaFetch(apiKey, '/generations/from-template', {
120
+ method: 'POST',
121
+ body: JSON.stringify(body),
122
+ });
123
+ }
124
+ /**
125
+ * Get generation status (including export URLs when available).
126
+ */
127
+ export async function getGenerationStatus(apiKey, generationId) {
128
+ return gammaFetch(apiKey, `/generations/${generationId}`);
129
+ }
130
+ /**
131
+ * List available themes.
132
+ */
133
+ export async function listThemes(apiKey, options) {
134
+ const params = new URLSearchParams();
135
+ if (options?.query)
136
+ params.set('query', options.query);
137
+ if (options?.limit)
138
+ params.set('limit', String(options.limit));
139
+ if (options?.after)
140
+ params.set('after', options.after);
141
+ const queryString = params.toString();
142
+ return gammaFetch(apiKey, `/themes${queryString ? `?${queryString}` : ''}`);
143
+ }
144
+ /**
145
+ * List workspace folders.
146
+ */
147
+ export async function listFolders(apiKey, options) {
148
+ const params = new URLSearchParams();
149
+ if (options?.query)
150
+ params.set('query', options.query);
151
+ if (options?.limit)
152
+ params.set('limit', String(options.limit));
153
+ if (options?.after)
154
+ params.set('after', options.after);
155
+ const queryString = params.toString();
156
+ return gammaFetch(apiKey, `/folders${queryString ? `?${queryString}` : ''}`);
157
+ }
158
+ /**
159
+ * Download an export file (PDF/PPTX) to the system tmpdir.
160
+ * Returns the absolute path of the downloaded file.
161
+ */
162
+ export async function downloadExportFile(url, generationId, format) {
163
+ const { writeFileSync } = await import('fs');
164
+ const { join } = await import('path');
165
+ const { tmpdir } = await import('os');
166
+ const safeId = generationId.replace(/[^a-zA-Z0-9_-]/g, '_');
167
+ const fileName = `gamma_export_${safeId}_${Date.now()}.${format}`;
168
+ const filePath = join(tmpdir(), fileName);
169
+ const response = await fetch(url);
170
+ if (!response.ok) {
171
+ throw new Error(`Download failed: HTTP ${response.status}`);
172
+ }
173
+ const buffer = Buffer.from(await response.arrayBuffer());
174
+ writeFileSync(filePath, buffer);
175
+ return filePath;
176
+ }
177
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gamma MCP Server
4
+ *
5
+ * Provides Gamma AI presentation generation via Model Context Protocol.
6
+ * Supports async generation workflow with export polling for PDF/PPTX.
7
+ *
8
+ * Environment variables:
9
+ * - GAMMA_API_KEY: Gamma API key (from https://gamma.app/settings/developers)
10
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
11
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
12
+ * - GAMMA_EXPORT_POLL_INTERVAL_MS: Export poll interval in ms (default: 5000)
13
+ * - GAMMA_EXPORT_POLL_MAX_ATTEMPTS: Max export poll attempts (default: 12)
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gamma MCP Server
4
+ *
5
+ * Provides Gamma AI presentation generation via Model Context Protocol.
6
+ * Supports async generation workflow with export polling for PDF/PPTX.
7
+ *
8
+ * Environment variables:
9
+ * - GAMMA_API_KEY: Gamma API key (from https://gamma.app/settings/developers)
10
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
11
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
12
+ * - GAMMA_EXPORT_POLL_INTERVAL_MS: Export poll interval in ms (default: 5000)
13
+ * - GAMMA_EXPORT_POLL_MAX_ATTEMPTS: Max export poll attempts (default: 12)
14
+ */
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { createServer } from './server.js';
17
+ async function main() {
18
+ const server = createServer();
19
+ const transport = new StdioServerTransport();
20
+ await server.connect(transport);
21
+ console.error('Gamma MCP server running on stdio');
22
+ }
23
+ main().catch((error) => {
24
+ console.error('Fatal error:', error);
25
+ process.exit(1);
26
+ });
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerGenerationTools, registerListingTools } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'gamma-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerGenerationTools(server);
10
+ registerListingTools(server);
11
+ return server;
12
+ }
13
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigureTools(server: McpServer): void;
3
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import { setApiKey } from '../auth.js';
3
+ import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
4
+ import { GammaError } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ export function registerConfigureTools(server) {
7
+ server.registerTool('configure_gamma_api_key', {
8
+ description: 'Configure the Gamma API key for this session. ' +
9
+ 'Only call this if you get an error saying "Gamma API key not configured". ' +
10
+ 'HOW USER GETS THEIR KEY: Go to https://gamma.app/settings/developers → Create API Key → Copy the key.',
11
+ inputSchema: z.object({
12
+ api_key: z.string().min(1).describe('The Gamma API key'),
13
+ }),
14
+ annotations: { readOnlyHint: false, destructiveHint: false },
15
+ }, withErrorHandling(async (args) => {
16
+ const key = args.api_key.trim();
17
+ // If bridge is available, persist via bridge
18
+ if (BRIDGE_STATE_PATH) {
19
+ try {
20
+ const result = await bridgeRequest('/bundled/gamma/configure', { apiKey: key });
21
+ if (result.success) {
22
+ setApiKey(key);
23
+ const message = result.warning
24
+ ? `Gamma API key configured successfully. Note: ${result.warning}`
25
+ : 'Gamma API key configured successfully! You can now use gamma_generate to create presentations, documents, and webpages.';
26
+ return JSON.stringify({ ok: true, message });
27
+ }
28
+ // Bridge returned failure — surface as error, do NOT fall through
29
+ throw new GammaError(result.error || 'Bridge configuration failed', 'BRIDGE_ERROR', 'The host app bridge rejected the configuration request. Check the host app logs.');
30
+ }
31
+ catch (error) {
32
+ if (error instanceof GammaError)
33
+ throw error;
34
+ // Bridge request failed (network, timeout, etc.) — surface as error
35
+ throw new GammaError(`Bridge request failed: ${error instanceof Error ? error.message : String(error)}`, 'BRIDGE_ERROR', 'Could not reach the host app bridge. Ensure the host app is running.');
36
+ }
37
+ }
38
+ // No bridge — store in-memory
39
+ setApiKey(key);
40
+ return JSON.stringify({
41
+ ok: true,
42
+ message: 'Gamma API key configured successfully! You can now use gamma_generate to create presentations, documents, and webpages.',
43
+ });
44
+ }));
45
+ }
46
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGenerationTools(server: McpServer): void;
3
+ //# sourceMappingURL=generation.d.ts.map
@@ -0,0 +1,298 @@
1
+ import { z } from 'zod';
2
+ import { getApiKey, hasApiKey } from '../auth.js';
3
+ import { createGeneration, createFromTemplate, getGenerationStatus, downloadExportFile, } from '../client.js';
4
+ import { GammaError, EXPORT_POLL_INTERVAL_MS, EXPORT_POLL_MAX_ATTEMPTS, } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+ /**
8
+ * In-memory map of generation_id → requested export format.
9
+ * Used to trigger extended polling when checking status.
10
+ */
11
+ const exportRequests = new Map();
12
+ function requireApiKey() {
13
+ if (!hasApiKey()) {
14
+ throw new GammaError('Gamma API key not configured', 'AUTH_REQUIRED', 'Ask the user for their Gamma API key (get it from https://gamma.app/settings/developers), then call configure_gamma_api_key to set it up.');
15
+ }
16
+ return getApiKey();
17
+ }
18
+ export function registerGenerationTools(server) {
19
+ // ── gamma_generate ─────────────────────────────────────────────
20
+ server.registerTool('gamma_generate', {
21
+ description: 'Create AI-powered presentations, documents, webpages, or social posts with Gamma. ' +
22
+ 'ASYNC WORKFLOW: 1) Call gamma_generate → returns generation_id. ' +
23
+ '2) Call gamma_get_status(generation_id) repeatedly every 3-5 seconds. ' +
24
+ '3) Continue polling until status is "completed" or "failed". ' +
25
+ 'THEMES: First call gamma_list_themes, then pass theme_id. ' +
26
+ 'EXPORT: Set export_as to "pdf" or "pptx" for auto-export on completion.',
27
+ inputSchema: z.object({
28
+ input_text: z.string().min(1).describe('The text content to generate from'),
29
+ format: z
30
+ .enum(['presentation', 'document', 'webpage', 'social'])
31
+ .optional()
32
+ .describe('Content type (default: presentation)'),
33
+ text_mode: z
34
+ .enum(['generate', 'condense', 'preserve'])
35
+ .optional()
36
+ .describe('generate: expand, condense: summarize, preserve: keep exact text'),
37
+ theme_id: z.string().optional().describe('Theme ID from gamma_list_themes'),
38
+ num_cards: z.number().optional().describe('Number of slides/cards (1-75)'),
39
+ card_split: z
40
+ .enum(['auto', 'inputTextBreaks'])
41
+ .optional()
42
+ .describe('How to divide cards'),
43
+ additional_instructions: z
44
+ .string()
45
+ .optional()
46
+ .describe('Extra instructions for generation (max 2000 chars)'),
47
+ folder_ids: z
48
+ .array(z.string())
49
+ .optional()
50
+ .describe('Folder IDs to save to'),
51
+ export_as: z
52
+ .enum(['pdf', 'pptx'])
53
+ .optional()
54
+ .describe('Auto-export format when generation completes'),
55
+ text_amount: z
56
+ .enum(['brief', 'medium', 'detailed', 'extensive'])
57
+ .optional()
58
+ .describe('How much text per card'),
59
+ text_tone: z.string().optional().describe('Tone/voice'),
60
+ text_audience: z.string().optional().describe('Target audience'),
61
+ text_language: z.string().optional().describe('Output language code'),
62
+ image_source: z
63
+ .enum([
64
+ 'aiGenerated',
65
+ 'pictographic',
66
+ 'unsplash',
67
+ 'giphy',
68
+ 'webAllImages',
69
+ 'webFreeToUse',
70
+ 'webFreeToUseCommercially',
71
+ 'placeholder',
72
+ 'noImages',
73
+ ])
74
+ .optional()
75
+ .describe('Image source'),
76
+ image_model: z.string().optional().describe('AI image model'),
77
+ image_style: z.string().optional().describe('Image style'),
78
+ card_dimensions: z
79
+ .enum(['fluid', '16x9', '4x3', 'pageless', 'letter', 'a4', '1x1', '4x5', '9x16'])
80
+ .optional()
81
+ .describe('Card aspect ratio'),
82
+ workspace_access: z
83
+ .enum(['noAccess', 'view', 'comment', 'edit', 'fullAccess'])
84
+ .optional()
85
+ .describe('Access level for workspace members'),
86
+ external_access: z
87
+ .enum(['noAccess', 'view', 'comment', 'edit'])
88
+ .optional()
89
+ .describe('Access level for external viewers'),
90
+ }),
91
+ annotations: { readOnlyHint: false, destructiveHint: false },
92
+ }, withErrorHandling(async (args) => {
93
+ const apiKey = requireApiKey();
94
+ // Build text options
95
+ const textOptions = args.text_amount || args.text_tone || args.text_audience || args.text_language
96
+ ? {
97
+ amount: args.text_amount,
98
+ tone: args.text_tone,
99
+ audience: args.text_audience,
100
+ language: args.text_language,
101
+ }
102
+ : undefined;
103
+ // Build image options
104
+ const imageOptions = args.image_source || args.image_model || args.image_style
105
+ ? {
106
+ source: args.image_source,
107
+ model: args.image_model,
108
+ style: args.image_style,
109
+ }
110
+ : undefined;
111
+ // Build card options
112
+ const cardOptions = args.card_dimensions
113
+ ? { dimensions: args.card_dimensions }
114
+ : undefined;
115
+ // Build sharing options
116
+ const sharingOptions = args.workspace_access || args.external_access
117
+ ? {
118
+ workspaceAccess: args.workspace_access,
119
+ externalAccess: args.external_access,
120
+ }
121
+ : undefined;
122
+ const result = await createGeneration(apiKey, {
123
+ inputText: args.input_text,
124
+ format: args.format || 'presentation',
125
+ textMode: args.text_mode || 'generate',
126
+ themeId: args.theme_id,
127
+ numCards: args.num_cards,
128
+ cardSplit: args.card_split,
129
+ additionalInstructions: args.additional_instructions,
130
+ folderIds: args.folder_ids,
131
+ exportAs: args.export_as,
132
+ textOptions,
133
+ imageOptions,
134
+ cardOptions,
135
+ sharingOptions,
136
+ });
137
+ if (args.export_as) {
138
+ exportRequests.set(result.generationId, args.export_as);
139
+ }
140
+ return JSON.stringify({
141
+ success: true,
142
+ generation_id: result.generationId,
143
+ message: `Generation started. Use gamma_get_status with ID "${result.generationId}" to check progress.`,
144
+ });
145
+ }));
146
+ // ── gamma_create_from_template ──────────────────────────────────
147
+ server.registerTool('gamma_create_from_template', {
148
+ description: 'Clone and modify an existing Gamma presentation/document using AI. ' +
149
+ 'Get the gamma_id from the URL (last part after title). ' +
150
+ 'ASYNC: Same workflow as gamma_generate — poll gamma_get_status after calling.',
151
+ inputSchema: z.object({
152
+ gamma_id: z.string().min(1).describe('The ID of the existing gamma to use as template'),
153
+ prompt: z.string().optional().describe('Instructions for how to modify the template'),
154
+ theme_id: z.string().optional().describe('Theme ID to apply'),
155
+ folder_ids: z.array(z.string()).optional().describe('Folder IDs to save to'),
156
+ export_as: z
157
+ .enum(['pdf', 'pptx'])
158
+ .optional()
159
+ .describe('Auto-export format'),
160
+ }),
161
+ annotations: { readOnlyHint: false, destructiveHint: false },
162
+ }, withErrorHandling(async (args) => {
163
+ const apiKey = requireApiKey();
164
+ const result = await createFromTemplate(apiKey, {
165
+ gammaId: args.gamma_id,
166
+ prompt: args.prompt,
167
+ themeId: args.theme_id,
168
+ folderIds: args.folder_ids,
169
+ exportAs: args.export_as,
170
+ });
171
+ if (args.export_as) {
172
+ exportRequests.set(result.generationId, args.export_as);
173
+ }
174
+ return JSON.stringify({
175
+ success: true,
176
+ generation_id: result.generationId,
177
+ message: `Template generation started. Use gamma_get_status with ID "${result.generationId}" to check progress.`,
178
+ });
179
+ }));
180
+ // ── gamma_get_status ───────────────────────────────────────────
181
+ server.registerTool('gamma_get_status', {
182
+ description: 'Poll the status of a Gamma generation. REQUIRED after calling gamma_generate or gamma_create_from_template. ' +
183
+ 'When export was requested, this tool automatically waits for the export URL. ' +
184
+ 'Status values: "pending" (call again in 3-5s), "completed" (done), "failed" (check error).',
185
+ inputSchema: z.object({
186
+ generation_id: z
187
+ .string()
188
+ .min(1)
189
+ .describe('The generation_id returned by gamma_generate or gamma_create_from_template'),
190
+ }),
191
+ annotations: { readOnlyHint: true },
192
+ }, withErrorHandling(async (args) => {
193
+ const apiKey = requireApiKey();
194
+ let status = await getGenerationStatus(apiKey, args.generation_id);
195
+ const response = {
196
+ generation_id: status.generationId,
197
+ status: status.status,
198
+ };
199
+ if (status.status === 'failed') {
200
+ exportRequests.delete(args.generation_id);
201
+ response.error = status.error;
202
+ response.message = 'Generation failed. Please try again.';
203
+ }
204
+ else if (status.status === 'completed') {
205
+ const exportFormat = exportRequests.get(args.generation_id);
206
+ const exportUrlKey = exportFormat === 'pdf' ? 'pdfUrl' : 'pptxUrl';
207
+ const hasExportUrl = exportFormat ? !!status[exportUrlKey] : false;
208
+ if (exportFormat && !hasExportUrl) {
209
+ // Delete Map entry before polling (concurrency guard)
210
+ exportRequests.delete(args.generation_id);
211
+ // Extended polling: export URL not yet available
212
+ for (let i = 1; i <= EXPORT_POLL_MAX_ATTEMPTS; i++) {
213
+ console.error(`[gamma] Waiting for export URL (attempt ${i}/${EXPORT_POLL_MAX_ATTEMPTS})...`);
214
+ await sleep(EXPORT_POLL_INTERVAL_MS);
215
+ try {
216
+ status = await getGenerationStatus(apiKey, args.generation_id);
217
+ }
218
+ catch (error) {
219
+ const errMsg = error instanceof Error ? error.message : String(error);
220
+ console.error(`[gamma] Polling error on attempt ${i}/${EXPORT_POLL_MAX_ATTEMPTS}: ${errMsg}`);
221
+ continue;
222
+ }
223
+ if (status.status === 'failed') {
224
+ response.status = 'failed';
225
+ response.error = status.error;
226
+ response.message = 'Generation failed. Please try again.';
227
+ return JSON.stringify(response, null, 2);
228
+ }
229
+ if (status[exportUrlKey]) {
230
+ break;
231
+ }
232
+ }
233
+ // After polling: check if URL appeared
234
+ const exportUrl = status[exportUrlKey];
235
+ if (exportUrl) {
236
+ response.gamma_url = status.gammaUrl;
237
+ if (status.pdfUrl)
238
+ response.pdf_url = status.pdfUrl;
239
+ if (status.pptxUrl)
240
+ response.pptx_url = status.pptxUrl;
241
+ if (status.credits)
242
+ response.credits = status.credits;
243
+ try {
244
+ const filePath = await downloadExportFile(exportUrl, args.generation_id, exportFormat);
245
+ response.file_path = filePath;
246
+ response.message = `Export file downloaded to ${filePath}. Use this local path — the URL will expire shortly.`;
247
+ }
248
+ catch (dlError) {
249
+ const dlMsg = dlError instanceof Error ? dlError.message : String(dlError);
250
+ console.error(`[gamma] Export download failed: ${dlMsg}`);
251
+ response.message = `Export URL is available but download failed: ${dlMsg}. The URL expires soon — download it immediately if needed.`;
252
+ }
253
+ }
254
+ else {
255
+ // Timeout — URL never appeared
256
+ response.gamma_url = status.gammaUrl;
257
+ if (status.credits)
258
+ response.credits = status.credits;
259
+ response.message = `Export file (${exportFormat}) was requested but the URL was not available after polling. The presentation was created successfully — you can export manually at: ${status.gammaUrl}`;
260
+ }
261
+ }
262
+ else {
263
+ // No export requested, or export URL already present
264
+ if (exportFormat) {
265
+ exportRequests.delete(args.generation_id);
266
+ }
267
+ response.gamma_url = status.gammaUrl;
268
+ if (status.pdfUrl)
269
+ response.pdf_url = status.pdfUrl;
270
+ if (status.pptxUrl)
271
+ response.pptx_url = status.pptxUrl;
272
+ if (status.credits)
273
+ response.credits = status.credits;
274
+ // Auto-download if export URL is present
275
+ if (exportFormat && status[exportUrlKey]) {
276
+ try {
277
+ const filePath = await downloadExportFile(status[exportUrlKey], args.generation_id, exportFormat);
278
+ response.file_path = filePath;
279
+ response.message = `Export file downloaded to ${filePath}. Use this local path — the URL will expire shortly.`;
280
+ }
281
+ catch (dlError) {
282
+ const dlMsg = dlError instanceof Error ? dlError.message : String(dlError);
283
+ console.error(`[gamma] Export download failed: ${dlMsg}`);
284
+ response.message = `Export URL is available but download failed: ${dlMsg}. The URL expires soon — download it immediately if needed.`;
285
+ }
286
+ }
287
+ else {
288
+ response.message = 'Generation complete! Access your content at the URL above.';
289
+ }
290
+ }
291
+ }
292
+ else {
293
+ response.message = 'Generation in progress...';
294
+ }
295
+ return JSON.stringify(response, null, 2);
296
+ }));
297
+ }
298
+ //# sourceMappingURL=generation.js.map
@@ -0,0 +1,4 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerGenerationTools } from './generation.js';
3
+ export { registerListingTools } from './listing.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerGenerationTools } from './generation.js';
3
+ export { registerListingTools } from './listing.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerListingTools(server: McpServer): void;
3
+ //# sourceMappingURL=listing.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+ import { getApiKey, hasApiKey } from '../auth.js';
3
+ import { listThemes, listFolders } from '../client.js';
4
+ import { GammaError } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ function requireApiKey() {
7
+ if (!hasApiKey()) {
8
+ throw new GammaError('Gamma API key not configured', 'AUTH_REQUIRED', 'Ask the user for their Gamma API key (get it from https://gamma.app/settings/developers), then call configure_gamma_api_key to set it up.');
9
+ }
10
+ return getApiKey();
11
+ }
12
+ export function registerListingTools(server) {
13
+ // ── gamma_list_themes ──────────────────────────────────────────
14
+ server.registerTool('gamma_list_themes', {
15
+ description: 'List available Gamma themes. Call this FIRST when user wants to apply corporate/custom branding. ' +
16
+ 'type: "custom" = workspace themes (corporate branding), type: "standard" = global themes. ' +
17
+ 'Use the "id" field when calling gamma_generate with theme_id parameter. ' +
18
+ 'Pagination: if has_more is true, pass next_cursor as the "after" parameter.',
19
+ inputSchema: z.object({
20
+ query: z.string().optional().describe('Search themes by name'),
21
+ limit: z.number().optional().describe('Results per page (default 50, max 50)'),
22
+ after: z.string().optional().describe('Pagination cursor from previous response'),
23
+ }),
24
+ annotations: { readOnlyHint: true },
25
+ }, withErrorHandling(async (args) => {
26
+ const apiKey = requireApiKey();
27
+ const result = await listThemes(apiKey, {
28
+ query: args.query,
29
+ limit: args.limit,
30
+ after: args.after,
31
+ });
32
+ return JSON.stringify({
33
+ themes: result.data,
34
+ has_more: result.hasMore,
35
+ next_cursor: result.nextCursor,
36
+ }, null, 2);
37
+ }));
38
+ // ── gamma_list_folders ─────────────────────────────────────────
39
+ server.registerTool('gamma_list_folders', {
40
+ description: 'List folders in the user\'s Gamma workspace for organizing presentations. ' +
41
+ 'Use folder "id" in gamma_generate\'s folder_ids parameter. ' +
42
+ 'Pagination: if has_more is true, pass next_cursor as the "after" parameter.',
43
+ inputSchema: z.object({
44
+ query: z.string().optional().describe('Search folders by name'),
45
+ limit: z.number().optional().describe('Results per page (default 50, max 50)'),
46
+ after: z.string().optional().describe('Pagination cursor from previous response'),
47
+ }),
48
+ annotations: { readOnlyHint: true },
49
+ }, withErrorHandling(async (args) => {
50
+ const apiKey = requireApiKey();
51
+ const result = await listFolders(apiKey, {
52
+ query: args.query,
53
+ limit: args.limit,
54
+ after: args.after,
55
+ });
56
+ return JSON.stringify({
57
+ folders: result.data,
58
+ has_more: result.hasMore,
59
+ next_cursor: result.nextCursor,
60
+ }, null, 2);
61
+ }));
62
+ }
63
+ //# sourceMappingURL=listing.js.map
@@ -0,0 +1,124 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ /** Export polling interval in ms — overridable via GAMMA_EXPORT_POLL_INTERVAL_MS */
3
+ export declare const EXPORT_POLL_INTERVAL_MS: number;
4
+ /** Max export polling attempts — overridable via GAMMA_EXPORT_POLL_MAX_ATTEMPTS */
5
+ export declare const EXPORT_POLL_MAX_ATTEMPTS: number;
6
+ export interface BridgeState {
7
+ port: number;
8
+ token: string;
9
+ }
10
+ export declare class GammaError extends Error {
11
+ readonly code: string;
12
+ readonly resolution: string;
13
+ constructor(message: string, code: string, resolution: string);
14
+ }
15
+ export type GenerationFormat = 'presentation' | 'document' | 'webpage' | 'social';
16
+ export type TextMode = 'generate' | 'condense' | 'preserve';
17
+ export type TextAmount = 'brief' | 'medium' | 'detailed' | 'extensive';
18
+ export type ImageSource = 'aiGenerated' | 'pictographic' | 'unsplash' | 'giphy' | 'webAllImages' | 'webFreeToUse' | 'webFreeToUseCommercially' | 'placeholder' | 'noImages';
19
+ export type CardDimensions = 'fluid' | '16x9' | '4x3' | 'pageless' | 'letter' | 'a4' | '1x1' | '4x5' | '9x16';
20
+ export type CardSplit = 'auto' | 'inputTextBreaks';
21
+ export type AccessLevel = 'noAccess' | 'view' | 'comment' | 'edit' | 'fullAccess';
22
+ export interface HeaderFooterItem {
23
+ type: 'text' | 'image' | 'cardNumber';
24
+ value?: string;
25
+ source?: 'themeLogo' | 'custom';
26
+ src?: string;
27
+ size?: 'sm' | 'md' | 'lg' | 'xl';
28
+ }
29
+ export interface HeaderFooterOptions {
30
+ topLeft?: HeaderFooterItem;
31
+ topRight?: HeaderFooterItem;
32
+ topCenter?: HeaderFooterItem;
33
+ bottomLeft?: HeaderFooterItem;
34
+ bottomRight?: HeaderFooterItem;
35
+ bottomCenter?: HeaderFooterItem;
36
+ hideFromFirstCard?: boolean;
37
+ hideFromLastCard?: boolean;
38
+ }
39
+ export interface GenerationRequest {
40
+ inputText: string;
41
+ format?: GenerationFormat;
42
+ textMode?: TextMode;
43
+ themeId?: string;
44
+ numCards?: number;
45
+ cardSplit?: CardSplit;
46
+ additionalInstructions?: string;
47
+ folderIds?: string[];
48
+ exportAs?: 'pdf' | 'pptx';
49
+ textOptions?: {
50
+ amount?: TextAmount;
51
+ tone?: string;
52
+ audience?: string;
53
+ language?: string;
54
+ };
55
+ imageOptions?: {
56
+ source?: ImageSource;
57
+ model?: string;
58
+ style?: string;
59
+ };
60
+ cardOptions?: {
61
+ dimensions?: CardDimensions;
62
+ headerFooter?: HeaderFooterOptions;
63
+ };
64
+ sharingOptions?: {
65
+ workspaceAccess?: AccessLevel;
66
+ externalAccess?: Exclude<AccessLevel, 'fullAccess'>;
67
+ emailOptions?: {
68
+ recipients?: string[];
69
+ access?: AccessLevel;
70
+ };
71
+ };
72
+ }
73
+ export interface CreateFromTemplateRequest {
74
+ gammaId: string;
75
+ prompt?: string;
76
+ themeId?: string;
77
+ folderIds?: string[];
78
+ exportAs?: 'pdf' | 'pptx';
79
+ imageOptions?: {
80
+ source?: ImageSource;
81
+ model?: string;
82
+ style?: string;
83
+ };
84
+ sharingOptions?: {
85
+ workspaceAccess?: AccessLevel;
86
+ externalAccess?: Exclude<AccessLevel, 'fullAccess'>;
87
+ emailOptions?: {
88
+ recipients?: string[];
89
+ access?: AccessLevel;
90
+ };
91
+ };
92
+ }
93
+ export interface GenerationResponse {
94
+ generationId: string;
95
+ }
96
+ export interface GenerationStatus {
97
+ generationId: string;
98
+ status: 'pending' | 'completed' | 'failed';
99
+ gammaUrl?: string;
100
+ pdfUrl?: string;
101
+ pptxUrl?: string;
102
+ credits?: {
103
+ deducted: number;
104
+ remaining: number;
105
+ };
106
+ error?: string;
107
+ }
108
+ export interface Theme {
109
+ id: string;
110
+ name: string;
111
+ type: 'standard' | 'custom';
112
+ colorKeywords?: string[];
113
+ toneKeywords?: string[];
114
+ }
115
+ export interface Folder {
116
+ id: string;
117
+ name: string;
118
+ }
119
+ export interface PaginatedResponse<T> {
120
+ data: T[];
121
+ hasMore: boolean;
122
+ nextCursor: string | null;
123
+ }
124
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ /** Export polling interval in ms — overridable via GAMMA_EXPORT_POLL_INTERVAL_MS */
3
+ export const EXPORT_POLL_INTERVAL_MS = parseInt(process.env.GAMMA_EXPORT_POLL_INTERVAL_MS ?? '5000', 10);
4
+ /** Max export polling attempts — overridable via GAMMA_EXPORT_POLL_MAX_ATTEMPTS */
5
+ export const EXPORT_POLL_MAX_ATTEMPTS = parseInt(process.env.GAMMA_EXPORT_POLL_MAX_ATTEMPTS ?? '12', 10);
6
+ export class GammaError extends Error {
7
+ code;
8
+ resolution;
9
+ constructor(message, code, resolution) {
10
+ super(message);
11
+ this.code = code;
12
+ this.resolution = resolution;
13
+ this.name = 'GammaError';
14
+ }
15
+ }
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On GammaError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { GammaError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On GammaError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof GammaError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-gamma",
3
+ "version": "0.1.0",
4
+ "description": "Gamma AI presentation generation MCP server for Model Context Protocol hosts",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-gamma": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/gamma"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/gamma",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
38
+ "@types/node": "^22",
39
+ "@vitest/coverage-v8": "^4.1.3",
40
+ "msw": "^2.13.2",
41
+ "shx": "^0.3.4",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^4.1.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }