posterly-mcp-server 0.1.1 → 0.2.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.
@@ -54,6 +54,12 @@ export declare class PosterlyClient {
54
54
  timezone?: string;
55
55
  count?: number;
56
56
  }): Promise<Slot[]>;
57
+ getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
58
+ upload_url: string;
59
+ token: string;
60
+ path: string;
61
+ public_url: string;
62
+ }>;
57
63
  uploadMedia(input: {
58
64
  filePath?: string;
59
65
  base64Data?: string;
@@ -62,20 +62,45 @@ export class PosterlyClient {
62
62
  const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
63
63
  return data.slots;
64
64
  }
65
+ async getSignedUploadUrl(filename, contentType, size) {
66
+ return this.request('POST', '/media/signed-upload', {
67
+ filename,
68
+ content_type: contentType,
69
+ size,
70
+ });
71
+ }
65
72
  async uploadMedia(input) {
66
- let data;
73
+ let fileBuffer;
67
74
  if (input.filePath) {
68
- const buffer = await readFile(resolve(input.filePath));
69
- data = buffer.toString('base64');
75
+ fileBuffer = await readFile(resolve(input.filePath));
70
76
  }
71
77
  else if (input.base64Data) {
72
- data = input.base64Data;
78
+ fileBuffer = Buffer.from(input.base64Data, 'base64');
73
79
  }
74
80
  else {
75
81
  throw new Error('Provide filePath or base64Data');
76
82
  }
77
83
  const contentType = input.contentType ||
78
84
  guessContentType(input.filename);
85
+ const SIZE_THRESHOLD = 3 * 1024 * 1024; // 3MB
86
+ if (fileBuffer.length > SIZE_THRESHOLD) {
87
+ // Large file: use signed URL to upload directly to Supabase Storage
88
+ const signed = await this.getSignedUploadUrl(input.filename, contentType, fileBuffer.length);
89
+ const uploadRes = await fetch(signed.upload_url, {
90
+ method: 'PUT',
91
+ headers: {
92
+ 'Content-Type': contentType,
93
+ },
94
+ body: new Uint8Array(fileBuffer),
95
+ });
96
+ if (!uploadRes.ok) {
97
+ const errText = await uploadRes.text().catch(() => uploadRes.statusText);
98
+ throw new Error(`Direct upload failed: ${errText}`);
99
+ }
100
+ return { url: signed.public_url, path: signed.path };
101
+ }
102
+ // Small file: use existing base64 flow through Vercel
103
+ const data = fileBuffer.toString('base64');
79
104
  return this.request('POST', '/media/upload', {
80
105
  filename: input.filename,
81
106
  data,
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const uploadMediaTool = {
3
3
  name: 'upload_media',
4
- description: 'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Max 5MB.',
4
+ description: 'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB.',
5
5
  inputSchema: z.object({
6
6
  file_path: z
7
7
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -119,19 +119,30 @@ export class PosterlyClient {
119
119
  return data.slots;
120
120
  }
121
121
 
122
+ async getSignedUploadUrl(
123
+ filename: string,
124
+ contentType: string,
125
+ size: number,
126
+ ): Promise<{ upload_url: string; token: string; path: string; public_url: string }> {
127
+ return this.request('POST', '/media/signed-upload', {
128
+ filename,
129
+ content_type: contentType,
130
+ size,
131
+ });
132
+ }
133
+
122
134
  async uploadMedia(input: {
123
135
  filePath?: string;
124
136
  base64Data?: string;
125
137
  filename: string;
126
138
  contentType?: string;
127
139
  }): Promise<{ url: string; path: string }> {
128
- let data: string;
140
+ let fileBuffer: Buffer;
129
141
 
130
142
  if (input.filePath) {
131
- const buffer = await readFile(resolve(input.filePath));
132
- data = buffer.toString('base64');
143
+ fileBuffer = await readFile(resolve(input.filePath));
133
144
  } else if (input.base64Data) {
134
- data = input.base64Data;
145
+ fileBuffer = Buffer.from(input.base64Data, 'base64');
135
146
  } else {
136
147
  throw new Error('Provide filePath or base64Data');
137
148
  }
@@ -140,6 +151,35 @@ export class PosterlyClient {
140
151
  input.contentType ||
141
152
  guessContentType(input.filename);
142
153
 
154
+ const SIZE_THRESHOLD = 3 * 1024 * 1024; // 3MB
155
+
156
+ if (fileBuffer.length > SIZE_THRESHOLD) {
157
+ // Large file: use signed URL to upload directly to Supabase Storage
158
+ const signed = await this.getSignedUploadUrl(
159
+ input.filename,
160
+ contentType,
161
+ fileBuffer.length,
162
+ );
163
+
164
+ const uploadRes = await fetch(signed.upload_url, {
165
+ method: 'PUT',
166
+ headers: {
167
+ 'Content-Type': contentType,
168
+ },
169
+ body: new Uint8Array(fileBuffer),
170
+ });
171
+
172
+ if (!uploadRes.ok) {
173
+ const errText = await uploadRes.text().catch(() => uploadRes.statusText);
174
+ throw new Error(`Direct upload failed: ${errText}`);
175
+ }
176
+
177
+ return { url: signed.public_url, path: signed.path };
178
+ }
179
+
180
+ // Small file: use existing base64 flow through Vercel
181
+ const data = fileBuffer.toString('base64');
182
+
143
183
  return this.request('POST', '/media/upload', {
144
184
  filename: input.filename,
145
185
  data,
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const uploadMediaTool = {
5
5
  name: 'upload_media',
6
6
  description:
7
- 'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Max 5MB.',
7
+ 'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB.',
8
8
  inputSchema: z.object({
9
9
  file_path: z
10
10
  .string()