1
2
3
4
5
6import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8import {
9 CallToolRequestSchema,
10 ListToolsRequestSchema,
11 Tool,
12} from '@modelcontextprotocol/sdk/types.js';
13import { Actor } from 'apify';
14import { z } from 'zod';
15
16import { Platform, Credentials, ActorInput, Invoice, InvoiceWithPdf } from './types.js';
17import { createPlatformHandler, SUPPORTED_PLATFORMS } from './platforms/index.js';
18import { logger } from './utils/logger.js';
19import { closeBrowser, BrowserOptions } from './utils/browser.js';
20
21
22const ListInvoicesSchema = z.object({
23 platform: z.enum(['vercel', 'digitalocean', 'railway']),
24});
25
26const DownloadInvoiceSchema = z.object({
27 platform: z.enum(['vercel', 'digitalocean', 'railway']),
28 invoiceId: z.string(),
29});
30
31const GetMetadataSchema = z.object({
32 platform: z.enum(['vercel', 'digitalocean', 'railway']),
33 invoiceId: z.string(),
34});
35
36
37const TOOLS: Tool[] = [
38 {
39 name: 'list_invoices',
40 description: 'List all invoices from a SaaS platform (Vercel, DigitalOcean, or Railway). Returns invoice IDs, dates, amounts, and status.',
41 inputSchema: {
42 type: 'object',
43 properties: {
44 platform: {
45 type: 'string',
46 enum: ['vercel', 'digitalocean', 'railway'],
47 description: 'The SaaS platform to list invoices from',
48 },
49 },
50 required: ['platform'],
51 },
52 },
53 {
54 name: 'download_invoice',
55 description: 'Download a specific invoice PDF from a SaaS platform. Returns a URL to the downloaded PDF stored in Apify key-value store.',
56 inputSchema: {
57 type: 'object',
58 properties: {
59 platform: {
60 type: 'string',
61 enum: ['vercel', 'digitalocean', 'railway'],
62 description: 'The SaaS platform to download invoice from',
63 },
64 invoiceId: {
65 type: 'string',
66 description: 'The ID of the invoice to download (from list_invoices)',
67 },
68 },
69 required: ['platform', 'invoiceId'],
70 },
71 },
72 {
73 name: 'get_invoice_metadata',
74 description: 'Get detailed metadata for a specific invoice without downloading the PDF.',
75 inputSchema: {
76 type: 'object',
77 properties: {
78 platform: {
79 type: 'string',
80 enum: ['vercel', 'digitalocean', 'railway'],
81 description: 'The SaaS platform',
82 },
83 invoiceId: {
84 type: 'string',
85 description: 'The ID of the invoice',
86 },
87 },
88 required: ['platform', 'invoiceId'],
89 },
90 },
91 {
92 name: 'list_platforms',
93 description: 'List all supported platforms and their configuration status.',
94 inputSchema: {
95 type: 'object',
96 properties: {},
97 },
98 },
99];
100
101export class InvoiceDownloaderServer {
102 private server: Server;
103 private credentials: ActorInput;
104 private browserOptions: BrowserOptions;
105 private invoiceCache: Map<string, Invoice[]> = new Map();
106
107 constructor(credentials: ActorInput) {
108 this.credentials = credentials;
109 this.browserOptions = {
110 headless: credentials.headless ?? true,
111 };
112
113 this.server = new Server(
114 {
115 name: 'invoice-downloader-mcp',
116 version: '1.0.0',
117 },
118 {
119 capabilities: {
120 tools: {},
121 },
122 }
123 );
124
125 this.setupHandlers();
126 }
127
128 private setupHandlers() {
129
130 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
131 tools: TOOLS,
132 }));
133
134
135 this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
136 const { name, arguments: args } = request.params;
137
138 try {
139 switch (name) {
140 case 'list_platforms':
141 return this.handleListPlatforms();
142
143 case 'list_invoices': {
144 const parsed = ListInvoicesSchema.parse(args);
145 return this.handleListInvoices(parsed.platform);
146 }
147
148 case 'download_invoice': {
149 const parsed = DownloadInvoiceSchema.parse(args);
150 return this.handleDownloadInvoice(parsed.platform, parsed.invoiceId);
151 }
152
153 case 'get_invoice_metadata': {
154 const parsed = GetMetadataSchema.parse(args);
155 return this.handleGetMetadata(parsed.platform, parsed.invoiceId);
156 }
157
158 default:
159 throw new Error(`Unknown tool: ${name}`);
160 }
161 } catch (error) {
162 logger.error(`Tool ${name} failed`, { error: String(error) });
163 return {
164 content: [
165 {
166 type: 'text',
167 text: JSON.stringify({ error: String(error) }),
168 },
169 ],
170 isError: true,
171 };
172 }
173 });
174 }
175
176 private handleListPlatforms() {
177 const platforms = SUPPORTED_PLATFORMS.map((p) => ({
178 name: p,
179 configured: !!this.credentials[p],
180 description: this.getPlatformDescription(p),
181 }));
182
183 return {
184 content: [
185 {
186 type: 'text',
187 text: JSON.stringify({ platforms }, null, 2),
188 },
189 ],
190 };
191 }
192
193 private getPlatformDescription(platform: Platform): string {
194 const descriptions: Record<Platform, string> = {
195 vercel: 'Vercel - Frontend cloud platform',
196 digitalocean: 'DigitalOcean - Cloud infrastructure provider',
197 railway: 'Railway - Modern deployment platform',
198 };
199 return descriptions[platform];
200 }
201
202 private async handleListInvoices(platform: Platform) {
203 const creds = this.credentials[platform];
204 if (!creds) {
205 throw new Error(`No credentials configured for ${platform}. Please provide credentials in Actor input.`);
206 }
207
208 const handler = createPlatformHandler(platform, this.browserOptions);
209
210 try {
211 const loggedIn = await handler.login(creds);
212 if (!loggedIn) {
213 throw new Error(`Failed to login to ${platform}`);
214 }
215
216 const invoices = await handler.listInvoices();
217
218
219 this.invoiceCache.set(platform, invoices);
220
221
222 await Actor.charge({ eventName: 'invoice-listed' });
223
224
225 await Actor.pushData({
226 action: 'list_invoices',
227 platform,
228 invoices,
229 fetchedAt: new Date().toISOString(),
230 });
231
232 return {
233 content: [
234 {
235 type: 'text',
236 text: JSON.stringify({
237 platform,
238 totalInvoices: invoices.length,
239 invoices,
240 }, null, 2),
241 },
242 ],
243 };
244 } finally {
245 await handler.close();
246 }
247 }
248
249 private async handleDownloadInvoice(platform: Platform, invoiceId: string) {
250 const creds = this.credentials[platform];
251 if (!creds) {
252 throw new Error(`No credentials configured for ${platform}`);
253 }
254
255 const handler = createPlatformHandler(platform, this.browserOptions);
256
257 try {
258 const loggedIn = await handler.login(creds);
259 if (!loggedIn) {
260 throw new Error(`Failed to login to ${platform}`);
261 }
262
263 const pdfBuffer = await handler.downloadInvoice(invoiceId);
264
265
266 const key = `${platform}-${invoiceId}.pdf`;
267 const store = await Actor.openKeyValueStore();
268 await store.setValue(key, pdfBuffer, { contentType: 'application/pdf' });
269
270
271 const storeId = (store as any).id || 'default';
272 const pdfUrl = `https://api.apify.com/v2/key-value-stores/${storeId}/records/${key}`;
273
274
275 await Actor.charge({ eventName: 'invoice-downloaded' });
276
277
278 const cachedInvoices = this.invoiceCache.get(platform);
279 const invoiceMeta = cachedInvoices?.find(i => i.id === invoiceId);
280
281 const result: InvoiceWithPdf = {
282 id: invoiceId,
283 platform,
284 date: invoiceMeta?.date || new Date().toISOString(),
285 amount: invoiceMeta?.amount || 0,
286 currency: invoiceMeta?.currency || 'USD',
287 status: invoiceMeta?.status || 'unknown',
288 pdfUrl,
289 downloadedAt: new Date().toISOString(),
290 };
291
292
293 await Actor.pushData({
294 action: 'download_invoice',
295 ...result,
296 });
297
298 return {
299 content: [
300 {
301 type: 'text',
302 text: JSON.stringify(result, null, 2),
303 },
304 ],
305 };
306 } finally {
307 await handler.close();
308 }
309 }
310
311 private async handleGetMetadata(platform: Platform, invoiceId: string) {
312
313 const cached = this.invoiceCache.get(platform);
314 const cachedInvoice = cached?.find(i => i.id === invoiceId);
315
316 if (cachedInvoice) {
317 return {
318 content: [
319 {
320 type: 'text',
321 text: JSON.stringify(cachedInvoice, null, 2),
322 },
323 ],
324 };
325 }
326
327
328 const creds = this.credentials[platform];
329 if (!creds) {
330 throw new Error(`No credentials configured for ${platform}`);
331 }
332
333 const handler = createPlatformHandler(platform, this.browserOptions);
334
335 try {
336 const loggedIn = await handler.login(creds);
337 if (!loggedIn) {
338 throw new Error(`Failed to login to ${platform}`);
339 }
340
341 const invoices = await handler.listInvoices();
342 this.invoiceCache.set(platform, invoices);
343
344 const invoice = invoices.find(i => i.id === invoiceId);
345 if (!invoice) {
346 throw new Error(`Invoice ${invoiceId} not found on ${platform}`);
347 }
348
349 return {
350 content: [
351 {
352 type: 'text',
353 text: JSON.stringify(invoice, null, 2),
354 },
355 ],
356 };
357 } finally {
358 await handler.close();
359 }
360 }
361
362 async run() {
363 const transport = new StdioServerTransport();
364 await this.server.connect(transport);
365 logger.info('Invoice Downloader MCP Server running');
366 }
367
368 async close() {
369 await closeBrowser();
370 await this.server.close();
371 }
372}