@upload-post/mcp 0.3.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/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +61 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/auth_resolver.d.ts +33 -0
- package/dist/oauth/auth_resolver.d.ts.map +1 -0
- package/dist/oauth/auth_resolver.js +30 -0
- package/dist/oauth/auth_resolver.js.map +1 -0
- package/dist/oauth/authorize.d.ts +18 -0
- package/dist/oauth/authorize.d.ts.map +1 -0
- package/dist/oauth/authorize.js +26 -0
- package/dist/oauth/authorize.js.map +1 -0
- package/dist/oauth/config.d.ts +19 -0
- package/dist/oauth/config.d.ts.map +1 -0
- package/dist/oauth/config.js +28 -0
- package/dist/oauth/config.js.map +1 -0
- package/dist/oauth/http_utils.d.ts +8 -0
- package/dist/oauth/http_utils.d.ts.map +1 -0
- package/dist/oauth/http_utils.js +48 -0
- package/dist/oauth/http_utils.js.map +1 -0
- package/dist/oauth/introspect_cache.d.ts +19 -0
- package/dist/oauth/introspect_cache.d.ts.map +1 -0
- package/dist/oauth/introspect_cache.js +40 -0
- package/dist/oauth/introspect_cache.js.map +1 -0
- package/dist/oauth/metadata.d.ts +35 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +43 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/registration.d.ts +14 -0
- package/dist/oauth/registration.d.ts.map +1 -0
- package/dist/oauth/registration.js +40 -0
- package/dist/oauth/registration.js.map +1 -0
- package/dist/oauth/tokens.d.ts +15 -0
- package/dist/oauth/tokens.d.ts.map +1 -0
- package/dist/oauth/tokens.js +43 -0
- package/dist/oauth/tokens.js.map +1 -0
- package/dist/oauth/upstream_client.d.ts +32 -0
- package/dist/oauth/upstream_client.d.ts.map +1 -0
- package/dist/oauth/upstream_client.js +62 -0
- package/dist/oauth/upstream_client.js.map +1 -0
- package/dist/schemas.d.ts +38 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +121 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +36 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/analytics.d.ts +4 -0
- package/dist/tools/analytics.d.ts.map +1 -0
- package/dist/tools/analytics.js +74 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/comments.d.ts +4 -0
- package/dist/tools/comments.d.ts.map +1 -0
- package/dist/tools/comments.js +108 -0
- package/dist/tools/comments.js.map +1 -0
- package/dist/tools/dms.d.ts +4 -0
- package/dist/tools/dms.d.ts.map +1 -0
- package/dist/tools/dms.js +100 -0
- package/dist/tools/dms.js.map +1 -0
- package/dist/tools/ffmpeg.d.ts +4 -0
- package/dist/tools/ffmpeg.d.ts.map +1 -0
- package/dist/tools/ffmpeg.js +96 -0
- package/dist/tools/ffmpeg.js.map +1 -0
- package/dist/tools/media_uploads.d.ts +4 -0
- package/dist/tools/media_uploads.d.ts.map +1 -0
- package/dist/tools/media_uploads.js +106 -0
- package/dist/tools/media_uploads.js.map +1 -0
- package/dist/tools/pages.d.ts +4 -0
- package/dist/tools/pages.d.ts.map +1 -0
- package/dist/tools/pages.js +101 -0
- package/dist/tools/pages.js.map +1 -0
- package/dist/tools/queue.d.ts +4 -0
- package/dist/tools/queue.d.ts.map +1 -0
- package/dist/tools/queue.js +79 -0
- package/dist/tools/queue.js.map +1 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.d.ts.map +1 -0
- package/dist/tools/schedule.js +48 -0
- package/dist/tools/schedule.js.map +1 -0
- package/dist/tools/status.d.ts +4 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +67 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/upload.d.ts +10 -0
- package/dist/tools/upload.d.ts.map +1 -0
- package/dist/tools/upload.js +397 -0
- package/dist/tools/upload.js.map +1 -0
- package/dist/tools/upload_studio.d.ts +3 -0
- package/dist/tools/upload_studio.d.ts.map +1 -0
- package/dist/tools/upload_studio.js +808 -0
- package/dist/tools/upload_studio.js.map +1 -0
- package/dist/tools/users.d.ts +4 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +95 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/transport/http.d.ts +33 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +263 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/stdio.d.ts +3 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +7 -0
- package/dist/transport/stdio.js.map +1 -0
- package/examples/mcp-config.json +11 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Upload-Post
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# @upload-post/mcp
|
|
2
|
+
|
|
3
|
+
Official **Model Context Protocol (MCP)** server for [Upload-Post](https://www.upload-post.com).
|
|
4
|
+
|
|
5
|
+
Lets any MCP-compatible AI agent (Claude Desktop, Claude Code, Cursor, …) publish, schedule, analyze and manage social media across **TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X, Google Business, Discord, Telegram and more** with a single API key.
|
|
6
|
+
|
|
7
|
+
> Built on top of the official [`upload-post`](https://www.npmjs.com/package/upload-post) SDK and the public Upload-Post REST API.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Two ways to use it
|
|
12
|
+
|
|
13
|
+
### A) Local stdio (single-user) — simplest
|
|
14
|
+
|
|
15
|
+
The server runs on your machine, spawned by the MCP client. Add to `~/.claude/mcp.json` (or Cursor settings, etc.):
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"upload-post": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@upload-post/mcp"],
|
|
23
|
+
"env": { "UPLOAD_POST_API_KEY": "YOUR_API_KEY" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Get your API key at <https://app.upload-post.com> → *API Keys*. Restart the client — you should see 45 `upload-post` tools.
|
|
30
|
+
|
|
31
|
+
### B) Hosted HTTP (multi-tenant) — share one server with many users
|
|
32
|
+
|
|
33
|
+
Run the server on any Docker-capable host (Fly, Railway, Cloud Run, your own box…) and let each user connect with **their own** Upload-Post API key. The server stores nothing per user.
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"upload-post": {
|
|
39
|
+
"url": "https://mcp.your-domain.com/mcp",
|
|
40
|
+
"headers": {
|
|
41
|
+
"Authorization": "ApiKey YOUR_OWN_UPLOAD_POST_API_KEY"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`Authorization: Bearer <key>` is also accepted, for clients that only allow Bearer.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What can the agent do?
|
|
53
|
+
|
|
54
|
+
The server exposes Upload-Post API tools plus one ChatGPT App UI launcher.
|
|
55
|
+
|
|
56
|
+
| Group | Tools |
|
|
57
|
+
|---------------|-------|
|
|
58
|
+
| Upload | `upload_video`, `upload_photos`, `upload_text`, `upload_document`, `open_upload_studio` |
|
|
59
|
+
| Media staging | `create_media_upload`, `complete_media_upload`, `get_media_upload`, `delete_media_upload` |
|
|
60
|
+
| Status | `get_status`, `get_job_status`, `get_history`, `get_media` |
|
|
61
|
+
| Schedule | `list_scheduled`, `cancel_scheduled`, `edit_scheduled` |
|
|
62
|
+
| Analytics | `get_analytics`, `get_total_impressions`, `get_post_analytics`, `get_platform_metrics` |
|
|
63
|
+
| Users | `get_account_info`, `list_users`, `create_user`, `delete_user`, `generate_jwt`, `validate_jwt` |
|
|
64
|
+
| Pages/boards | `get_facebook_pages`, `get_linkedin_pages`, `get_pinterest_boards`, `get_google_business_locations`, `select_google_business_location`, `get_reddit_detailed_posts` |
|
|
65
|
+
| Comments | `get_post_comments`, `reply_to_comment`, `public_reply_to_comment` |
|
|
66
|
+
| DMs | `send_dm`, `list_dm_conversations`, `manage_autodms` |
|
|
67
|
+
| FFmpeg | `submit_ffmpeg_job`, `get_ffmpeg_job`, `download_ffmpeg_result`, `get_ffmpeg_consumption` |
|
|
68
|
+
| Queue | `get_queue_settings`, `update_queue_settings`, `preview_queue` |
|
|
69
|
+
|
|
70
|
+
Async uploads return a `request_id`. The agent should poll `get_status` until `success: true`.
|
|
71
|
+
|
|
72
|
+
FFmpeg jobs accept one public URL through `input_url` or multiple URLs through `files`. Poll `get_ffmpeg_job` until completion, then call `download_ffmpeg_result`; it returns the result URL without streaming the processed binary through MCP.
|
|
73
|
+
|
|
74
|
+
### ChatGPT video upload UI
|
|
75
|
+
|
|
76
|
+
`open_upload_studio` renders a ChatGPT Apps component for file-based video publishing. The widget creates a short-lived Upload-Post/R2 staging upload, PUTs the local video directly to R2, completes the upload, then calls `upload_video` with the returned temporary media URL.
|
|
77
|
+
|
|
78
|
+
The staging object is deleted after 24 hours whether it is used or not. Scheduled/queued posts remain safe because `upload_video` copies the temporary URL into the existing durable scheduler storage before execution.
|
|
79
|
+
|
|
80
|
+
Claude and other MCP clients can use the same flow without the ChatGPT UI: call `create_media_upload`, PUT the file to `upload_url`, call `complete_media_upload`, then pass `media_url` to `upload_video`.
|
|
81
|
+
|
|
82
|
+
Set `UPLOAD_POST_R2_CONNECT_DOMAINS` on the MCP host to the comma-separated origins used by the backend's R2 signed URLs when they differ from the defaults (for example `https://<account>.r2.cloudflarestorage.com,https://<bucket>.<account>.r2.cloudflarestorage.com`) so the ChatGPT component CSP allows the browser PUT.
|
|
83
|
+
|
|
84
|
+
The R2 bucket CORS policy must allow browser uploads. A restrictive policy can include your actual widget origin; for fastest validation, use:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
[
|
|
88
|
+
{
|
|
89
|
+
"AllowedOrigins": ["*"],
|
|
90
|
+
"AllowedMethods": ["PUT", "GET", "HEAD"],
|
|
91
|
+
"AllowedHeaders": ["*"],
|
|
92
|
+
"ExposeHeaders": ["ETag"],
|
|
93
|
+
"MaxAgeSeconds": 3600
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Local / development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/Upload-Post/upload-post-mcp.git
|
|
104
|
+
cd upload-post-mcp
|
|
105
|
+
npm install
|
|
106
|
+
npm run build
|
|
107
|
+
|
|
108
|
+
# stdio (default — used by Claude Desktop, Cursor)
|
|
109
|
+
UPLOAD_POST_API_KEY=... node dist/index.js
|
|
110
|
+
|
|
111
|
+
# HTTP streamable (for hosted deployments)
|
|
112
|
+
UPLOAD_POST_API_KEY=... node dist/index.js --http --port 8080
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Inspect the live tool surface with the official inspector:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
| Env var | Mode | Default | Description |
|
|
126
|
+
|---------------------------|---------|------------------------------------|------------------------------------------|
|
|
127
|
+
| `UPLOAD_POST_API_KEY` | stdio | — (required) | Single user's Upload-Post API key. **Ignored in `--http` mode** — keys come per request. |
|
|
128
|
+
| `UPLOAD_POST_BASE_URL` | both | `https://api.upload-post.com/api` | Override for self-hosted / staging. |
|
|
129
|
+
| `UPLOAD_POST_MCP_PORT` | http | `8080` | Port for `--http` mode. |
|
|
130
|
+
| `OPENAI_APPS_CHALLENGE_TOKEN` | http | Current Upload-Post challenge token | Optional override for ChatGPT Apps domain verification at `/.well-known/openai-apps-challenge`. |
|
|
131
|
+
|
|
132
|
+
CLI flags:
|
|
133
|
+
|
|
134
|
+
- `--http` — start the streamable HTTP transport instead of stdio
|
|
135
|
+
- `--port <n>` — port for HTTP mode
|
|
136
|
+
|
|
137
|
+
HTTP endpoints:
|
|
138
|
+
|
|
139
|
+
- `POST /mcp` — JSON-RPC over MCP streamable HTTP. **Requires `Authorization: ApiKey <key>` (or `Bearer <key>`)** on every request. The key is the user's own Upload-Post API key; the server uses it only for that session and stores nothing.
|
|
140
|
+
- `GET /healthz` — liveness probe, always open. Returns `{"ok":true}`.
|
|
141
|
+
|
|
142
|
+
Auth model in `--http` mode is the same pattern Resend, Tavily, Brave Search and other API-key-native services use for their hosted MCPs: the upstream key *is* the auth.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Deploy with Docker
|
|
147
|
+
|
|
148
|
+
The repo ships with a multi-stage `Dockerfile` and a `.dockerignore`. On any Docker-capable PaaS (Fly.io, Railway, Render, Cloud Run, fly machines, your own box…):
|
|
149
|
+
|
|
150
|
+
1. Point the PaaS at this repo and select **Dockerfile** as the build pack.
|
|
151
|
+
2. **Port: 8080** (matches `EXPOSE 8080`).
|
|
152
|
+
3. **Environment variables**: none are required. Optionally set `UPLOAD_POST_BASE_URL` if you point at staging.
|
|
153
|
+
4. **Health check path**: `/healthz` (HTTP, port 8080).
|
|
154
|
+
5. **Domain**: attach a domain, e.g. `mcp.your-domain.com`, and provision TLS (most PaaS do this automatically via Let's Encrypt).
|
|
155
|
+
|
|
156
|
+
Deploy. The server is now ready for any number of users. Each user adds the endpoint to their MCP client config with **their own** Upload-Post API key:
|
|
157
|
+
|
|
158
|
+
```jsonc
|
|
159
|
+
{
|
|
160
|
+
"mcpServers": {
|
|
161
|
+
"upload-post": {
|
|
162
|
+
"url": "https://mcp.your-domain.com/mcp",
|
|
163
|
+
"headers": {
|
|
164
|
+
"Authorization": "ApiKey USER_OWN_UPLOAD_POST_API_KEY"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
> Without an `Authorization` header the server returns `401`. The header is the only credential — invalid Upload-Post keys will surface as upstream errors on the first tool call.
|
|
172
|
+
|
|
173
|
+
Local test of the production image:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
docker build -t upload-post-mcp .
|
|
177
|
+
docker run --rm -p 8080:8080 upload-post-mcp
|
|
178
|
+
curl http://localhost:8080/healthz # → {"ok":true}
|
|
179
|
+
curl -i -X POST http://localhost:8080/mcp \
|
|
180
|
+
-H "content-type: application/json" \
|
|
181
|
+
-d '{}' # → 401 (no Authorization)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Tips for prompting the agent
|
|
187
|
+
|
|
188
|
+
- Prefer **public URLs** over local paths when uploading — local paths only work if the MCP server runs on the user's machine.
|
|
189
|
+
- In ChatGPT Apps, prefer `open_upload_studio` for user-selected video files. It avoids local-path handoff issues by uploading to short-lived Upload-Post/R2 staging, then passing a temporary media URL to `upload_video`.
|
|
190
|
+
- To send video **bytes directly** (a client that holds the file rather than a URL), pass `videoBase64` to `upload_video` instead of `videoPathOrUrl`. The server writes it to a temp file, uploads, then deletes it. Inline bytes are capped at `UPLOAD_POST_MAX_INLINE_MB` (default 100 MB) — for larger videos use a public URL.
|
|
191
|
+
- Always create the profile first (`create_user`) and connect socials in the Upload-Post dashboard before publishing.
|
|
192
|
+
- For scheduled posts, pass ISO 8601 dates with timezone, e.g. `"2026-12-25T10:00:00Z"` + `"timezone": "Europe/Madrid"`.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Privacy & data handling
|
|
197
|
+
|
|
198
|
+
This server is a **stateless proxy** to the Upload-Post API. Per request, the only data it processes is the user's API key (or OAuth access token resolved to one) and the arguments of the tool call being executed. No user data is persisted by the MCP container itself.
|
|
199
|
+
|
|
200
|
+
- **What we receive per request**: the `Authorization` header, the MCP tool name + arguments, and any media URLs/paths the agent passes.
|
|
201
|
+
- **What we forward**: the tool arguments to the Upload-Post API on behalf of the authenticated user.
|
|
202
|
+
- **What we store**: nothing per-user. OAuth tokens are stored upstream in the Upload-Post backend, hashed (SHA-256), so a breach of token storage cannot impersonate users.
|
|
203
|
+
- **What we log**: HTTP method, path, status code, and an opaque request ID. No tool arguments, no API keys, no tokens.
|
|
204
|
+
|
|
205
|
+
Full Upload-Post privacy policy (data collection, retention, third-party sharing, contact, GDPR/CCPA): **https://upload-post.com/privacy**
|
|
206
|
+
|
|
207
|
+
To revoke a connector's access at any time, open **Connected Apps** in [app.upload-post.com](https://app.upload-post.com).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Security
|
|
212
|
+
|
|
213
|
+
- All traffic is TLS-terminated at the edge (HTTPS only).
|
|
214
|
+
- `/mcp` requires a valid `Authorization` header on every request; OAuth access tokens are short-lived (1 h access + 90 d refresh with rotation per RFC 6749 §10.4).
|
|
215
|
+
- The server validates the `Origin` header against an allow-list (`claude.ai`, `claude.com`, `chatgpt.com`, `chat.openai.com`, `app.upload-post.com`, `localhost`) to mitigate DNS-rebinding attacks from browser-based clients. Extend with `OAUTH_EXTRA_ALLOWED_ORIGINS` (comma-separated) when self-hosting behind a custom dashboard.
|
|
216
|
+
- If ChatGPT shows `redirect_uri not on allow-list` during OAuth, add the exact `redirect_uri` from the failing authorize request to the Upload-Post backend OAuth redirect allow-list. For ChatGPT clients this is typically on `https://chatgpt.com/.../oauth/callback` or `https://chat.openai.com/.../oauth/callback`.
|
|
217
|
+
- All tools declare MCP `readOnlyHint`/`destructiveHint` annotations so clients can surface confirmation prompts for destructive operations.
|
|
218
|
+
|
|
219
|
+
Report a security issue: **info@upload-post.com** (encrypted PGP available on request).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT © Upload-Post
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AxiosInstance } from "axios";
|
|
2
|
+
import UploadPost from "upload-post";
|
|
3
|
+
export declare const PACKAGE_VERSION = "0.1.1";
|
|
4
|
+
export declare const DEFAULT_BASE_URL = "https://api.upload-post.com/api";
|
|
5
|
+
export interface UploadPostMcpClientOptions {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Single facade used by every MCP tool.
|
|
11
|
+
*
|
|
12
|
+
* - `sdk` — the official `upload-post` npm SDK (typed methods for ~28 endpoints).
|
|
13
|
+
* - `http` — pre-configured axios instance for the ~20 endpoints not yet exposed
|
|
14
|
+
* by the SDK (DMs, autodms, teams, queue, retry, FFmpeg jobs, status page,
|
|
15
|
+
* AI rewrite, Reddit metadata, growth snapshots, …).
|
|
16
|
+
*
|
|
17
|
+
* The MCP layer never reaches Upload-Post by any other route.
|
|
18
|
+
*/
|
|
19
|
+
export declare class UploadPostMcpClient {
|
|
20
|
+
readonly sdk: UploadPost;
|
|
21
|
+
readonly http: AxiosInstance;
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
constructor(opts: UploadPostMcpClientOptions);
|
|
24
|
+
/** Thin wrapper that surfaces non-2xx as Errors carrying the API payload. */
|
|
25
|
+
request<T = unknown>(method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT", path: string, options?: {
|
|
26
|
+
body?: unknown;
|
|
27
|
+
query?: Record<string, unknown>;
|
|
28
|
+
}): Promise<T>;
|
|
29
|
+
}
|
|
30
|
+
/** Strip undefined keys so we don't send `?foo=undefined` upstream. */
|
|
31
|
+
export declare function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;
|
|
32
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAsB,MAAM,OAAO,CAAC;AACjE,OAAO,UAAU,MAAM,aAAa,CAAC;AAErC,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,gBAAgB,oCAAoC,CAAC;AAElE,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,0BAA0B;IAmB5C,6EAA6E;IACvE,OAAO,CAAC,CAAC,GAAG,OAAO,EACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAO,GAChE,OAAO,CAAC,CAAC,CAAC;CAiBd;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAQ7E"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import UploadPost from "upload-post";
|
|
3
|
+
export const PACKAGE_VERSION = "0.1.1";
|
|
4
|
+
export const DEFAULT_BASE_URL = "https://api.upload-post.com/api";
|
|
5
|
+
/**
|
|
6
|
+
* Single facade used by every MCP tool.
|
|
7
|
+
*
|
|
8
|
+
* - `sdk` — the official `upload-post` npm SDK (typed methods for ~28 endpoints).
|
|
9
|
+
* - `http` — pre-configured axios instance for the ~20 endpoints not yet exposed
|
|
10
|
+
* by the SDK (DMs, autodms, teams, queue, retry, FFmpeg jobs, status page,
|
|
11
|
+
* AI rewrite, Reddit metadata, growth snapshots, …).
|
|
12
|
+
*
|
|
13
|
+
* The MCP layer never reaches Upload-Post by any other route.
|
|
14
|
+
*/
|
|
15
|
+
export class UploadPostMcpClient {
|
|
16
|
+
sdk;
|
|
17
|
+
http;
|
|
18
|
+
baseUrl;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
if (!opts.apiKey) {
|
|
21
|
+
throw new Error("UPLOAD_POST_API_KEY is required. Get one at https://app.upload-post.com");
|
|
22
|
+
}
|
|
23
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
24
|
+
this.sdk = new UploadPost(opts.apiKey);
|
|
25
|
+
this.http = axios.create({
|
|
26
|
+
baseURL: this.baseUrl,
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Apikey ${opts.apiKey}`,
|
|
29
|
+
"X-Upload-Post-Source": `mcp/${PACKAGE_VERSION}`,
|
|
30
|
+
},
|
|
31
|
+
timeout: 120_000,
|
|
32
|
+
validateStatus: () => true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Thin wrapper that surfaces non-2xx as Errors carrying the API payload. */
|
|
36
|
+
async request(method, path, options = {}) {
|
|
37
|
+
const config = {
|
|
38
|
+
method,
|
|
39
|
+
url: path,
|
|
40
|
+
params: options.query,
|
|
41
|
+
data: options.body,
|
|
42
|
+
};
|
|
43
|
+
const res = await this.http.request(config);
|
|
44
|
+
if (res.status < 200 || res.status >= 300) {
|
|
45
|
+
const body = typeof res.data === "string" ? res.data : JSON.stringify(res.data);
|
|
46
|
+
throw new Error(`Upload-Post API ${method} ${path} failed (${res.status}): ${body}`);
|
|
47
|
+
}
|
|
48
|
+
return res.data;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Strip undefined keys so we don't send `?foo=undefined` upstream. */
|
|
52
|
+
export function compact(obj) {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
55
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
56
|
+
out[k] = v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAA4C,MAAM,OAAO,CAAC;AACjE,OAAO,UAAU,MAAM,aAAa,CAAC;AAErC,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,iCAAiC,CAAC;AAOlE;;;;;;;;;GASG;AACH,MAAM,OAAO,mBAAmB;IACrB,GAAG,CAAa;IAChB,IAAI,CAAgB;IACpB,OAAO,CAAS;IAEzB,YAAY,IAAgC;QAC1C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;gBACtC,sBAAsB,EAAE,OAAO,eAAe,EAAE;aACjD;YACD,OAAO,EAAE,OAAO;YAChB,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,OAAO,CACX,MAAmD,EACnD,IAAY,EACZ,UAA+D,EAAE;QAEjE,MAAM,MAAM,GAAuB;YACjC,MAAM;YACN,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,OAAO,CAAC,KAAK;YACrB,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAI,MAAM,CAAC,CAAC;QAC/C,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAC1C,MAAM,IAAI,GACR,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACrE,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,IAAI,IAAI,YAAY,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,CACpE,CAAC;QACJ,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,UAAU,OAAO,CAAoC,GAAM;IAC/D,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;YAC7C,GAA+B,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { UploadPostMcpClient } from "./client.js";
|
|
3
|
+
import { buildServer } from "./server.js";
|
|
4
|
+
import { runStdio } from "./transport/stdio.js";
|
|
5
|
+
import { runHttp } from "./transport/http.js";
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const out = {
|
|
8
|
+
http: false,
|
|
9
|
+
port: Number(process.env.UPLOAD_POST_MCP_PORT ?? 8080),
|
|
10
|
+
};
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const arg = argv[i];
|
|
13
|
+
if (arg === "--http")
|
|
14
|
+
out.http = true;
|
|
15
|
+
else if (arg === "--port")
|
|
16
|
+
out.port = Number(argv[++i]);
|
|
17
|
+
else if (arg === "--help" || arg === "-h") {
|
|
18
|
+
process.stdout.write([
|
|
19
|
+
"upload-post-mcp — Model Context Protocol server for Upload-Post",
|
|
20
|
+
"",
|
|
21
|
+
"Usage: upload-post-mcp [--http] [--port <n>]",
|
|
22
|
+
"",
|
|
23
|
+
"Modes:",
|
|
24
|
+
" stdio (default) single-user, API key from UPLOAD_POST_API_KEY env var",
|
|
25
|
+
" --http multi-tenant, API key per request via Authorization header",
|
|
26
|
+
"",
|
|
27
|
+
"Env:",
|
|
28
|
+
" UPLOAD_POST_API_KEY Required in stdio mode. Ignored in --http mode.",
|
|
29
|
+
" UPLOAD_POST_BASE_URL (optional) Override API base URL",
|
|
30
|
+
" UPLOAD_POST_MCP_PORT (optional) HTTP port (default 8080)",
|
|
31
|
+
"",
|
|
32
|
+
].join("\n"));
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
async function main() {
|
|
39
|
+
const args = parseArgs(process.argv.slice(2));
|
|
40
|
+
const baseUrl = process.env.UPLOAD_POST_BASE_URL;
|
|
41
|
+
if (args.http) {
|
|
42
|
+
// Multi-tenant: each request brings its own Upload-Post API key in the
|
|
43
|
+
// Authorization header. No shared key on the server.
|
|
44
|
+
await runHttp({
|
|
45
|
+
port: args.port,
|
|
46
|
+
baseUrl,
|
|
47
|
+
buildServer,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Single-user stdio: key comes from env, baked into one shared client.
|
|
52
|
+
const apiKey = process.env.UPLOAD_POST_API_KEY;
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
process.stderr.write("[upload-post-mcp] UPLOAD_POST_API_KEY env var is required in stdio mode.\n" +
|
|
55
|
+
"Set it in your MCP client config. Get one at https://app.upload-post.com\n");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const client = new UploadPostMcpClient({ apiKey, baseUrl });
|
|
59
|
+
const server = buildServer(client);
|
|
60
|
+
await runStdio(server);
|
|
61
|
+
}
|
|
62
|
+
main().catch((err) => {
|
|
63
|
+
process.stderr.write(`[upload-post-mcp] fatal: ${err?.stack ?? err}\n`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAO9C,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,GAAG,GAAe;QACtB,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,IAAI,CAAC;KACvD,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,QAAQ;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;aACjC,IAAI,GAAG,KAAK,QAAQ;YAAE,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aACnD,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;gBACE,iEAAiE;gBACjE,EAAE;gBACF,8CAA8C;gBAC9C,EAAE;gBACF,QAAQ;gBACR,2EAA2E;gBAC3E,gFAAgF;gBAChF,EAAE;gBACF,MAAM;gBACN,0EAA0E;gBAC1E,2DAA2D;gBAC3D,8DAA8D;gBAC9D,EAAE;aACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAEjD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,uEAAuE;QACvE,qDAAqD;QACrD,MAAM,OAAO,CAAC;YACZ,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO;YACP,WAAW;SACZ,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,uEAAuE;IACvE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,4EAA4E;YAC1E,4EAA4E,CAC/E,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,mBAAmB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;IACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OAuthConfig } from "./config.js";
|
|
2
|
+
import type { UpstreamOAuthClient } from "./upstream_client.js";
|
|
3
|
+
import type { IntrospectCache } from "./introspect_cache.js";
|
|
4
|
+
/**
|
|
5
|
+
* The /mcp endpoint accepts THREE Authorization header shapes:
|
|
6
|
+
*
|
|
7
|
+
* 1. `Authorization: ApiKey <raw_upload_post_api_key>` — historical, used by
|
|
8
|
+
* Claude Desktop / Code / Cursor with mcp.json. Returns the key verbatim.
|
|
9
|
+
*
|
|
10
|
+
* 2. `Authorization: Bearer up_oauth_<...>` — OAuth access token issued via
|
|
11
|
+
* DCR + authorization-code+PKCE. Introspected against the Upload-Post
|
|
12
|
+
* backend (cached ~60s) to recover the underlying API key.
|
|
13
|
+
*
|
|
14
|
+
* 3. `Authorization: Bearer <raw_upload_post_api_key>` — historical fallback
|
|
15
|
+
* for clients that only allow Bearer. Returns the key verbatim.
|
|
16
|
+
*
|
|
17
|
+
* Any of these end with the same shape: `{ apiKey, source }`. The session-
|
|
18
|
+
* scoped UploadPostMcpClient is then built with that key. Existing clients
|
|
19
|
+
* see zero behavior change.
|
|
20
|
+
*/
|
|
21
|
+
export type AuthResolution = {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
source: "apikey" | "bearer_raw" | "oauth";
|
|
24
|
+
email?: string;
|
|
25
|
+
clientId?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
export interface AuthResolverDeps {
|
|
28
|
+
cfg: OAuthConfig;
|
|
29
|
+
upstream: UpstreamOAuthClient;
|
|
30
|
+
cache: IntrospectCache;
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveAuth(header: string | string[] | undefined, deps: AuthResolverDeps): Promise<AuthResolution>;
|
|
33
|
+
//# sourceMappingURL=auth_resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth_resolver.d.ts","sourceRoot":"","sources":["../../src/oauth/auth_resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,cAAc,GACtB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAChG,IAAI,CAAC;AAIT,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,WAAW,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EACrC,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CA0BzB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const OAUTH_ACCESS_PREFIX = "up_oauth_";
|
|
2
|
+
export async function resolveAuth(header, deps) {
|
|
3
|
+
if (!header || Array.isArray(header))
|
|
4
|
+
return null;
|
|
5
|
+
const match = header.match(/^(ApiKey|Bearer)\s+(.+)$/i);
|
|
6
|
+
if (!match)
|
|
7
|
+
return null;
|
|
8
|
+
const scheme = match[1].toLowerCase();
|
|
9
|
+
const token = match[2].trim();
|
|
10
|
+
if (!token)
|
|
11
|
+
return null;
|
|
12
|
+
if (scheme === "apikey") {
|
|
13
|
+
return { apiKey: token, source: "apikey" };
|
|
14
|
+
}
|
|
15
|
+
// scheme === "bearer"
|
|
16
|
+
if (!token.startsWith(OAUTH_ACCESS_PREFIX) || !deps.cfg.enabled) {
|
|
17
|
+
return { apiKey: token, source: "bearer_raw" };
|
|
18
|
+
}
|
|
19
|
+
// OAuth opaque access token — introspect (with cache).
|
|
20
|
+
const cached = deps.cache.get(token);
|
|
21
|
+
if (cached) {
|
|
22
|
+
return { apiKey: cached.api_key, source: "oauth", email: cached.email, clientId: cached.client_id };
|
|
23
|
+
}
|
|
24
|
+
const fresh = await deps.upstream.introspect(token);
|
|
25
|
+
if (!fresh)
|
|
26
|
+
return null;
|
|
27
|
+
deps.cache.set(token, fresh);
|
|
28
|
+
return { apiKey: fresh.api_key, source: "oauth", email: fresh.email, clientId: fresh.client_id };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=auth_resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth_resolver.js","sourceRoot":"","sources":["../../src/oauth/auth_resolver.ts"],"names":[],"mappings":"AA0BA,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAQxC,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAqC,EACrC,IAAsB;IAEtB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAC7C,CAAC;IAED,sBAAsB;IACtB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAChE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACjD,CAAC;IAED,uDAAuD;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;IACtG,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;AACnG,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { OAuthConfig } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* GET /authorize
|
|
5
|
+
*
|
|
6
|
+
* Pure redirect to the dashboard's consent page (app.upload-post.com/oauth/authorize),
|
|
7
|
+
* carrying all OAuth params verbatim. The dashboard is where the user actually logs
|
|
8
|
+
* in (if needed) and approves — it then calls back to the Upload-Post
|
|
9
|
+
* backend to mint an authorization code, and finally redirects the user
|
|
10
|
+
* agent to the client-provided `redirect_uri` with the code.
|
|
11
|
+
*
|
|
12
|
+
* Doing it as a 302 (instead of rendering the consent screen here) means:
|
|
13
|
+
* - the user's existing app.upload-post.com session cookie/JWT is reachable
|
|
14
|
+
* - we don't duplicate login UI in this Node server
|
|
15
|
+
* - the MCP server stays purely API-shaped
|
|
16
|
+
*/
|
|
17
|
+
export declare function handleAuthorize(req: IncomingMessage, res: ServerResponse, cfg: OAuthConfig): void;
|
|
18
|
+
//# sourceMappingURL=authorize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/oauth/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,WAAW,GACf,IAAI,CAQN"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { sendError, sendRedirect } from "./http_utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* GET /authorize
|
|
4
|
+
*
|
|
5
|
+
* Pure redirect to the dashboard's consent page (app.upload-post.com/oauth/authorize),
|
|
6
|
+
* carrying all OAuth params verbatim. The dashboard is where the user actually logs
|
|
7
|
+
* in (if needed) and approves — it then calls back to the Upload-Post
|
|
8
|
+
* backend to mint an authorization code, and finally redirects the user
|
|
9
|
+
* agent to the client-provided `redirect_uri` with the code.
|
|
10
|
+
*
|
|
11
|
+
* Doing it as a 302 (instead of rendering the consent screen here) means:
|
|
12
|
+
* - the user's existing app.upload-post.com session cookie/JWT is reachable
|
|
13
|
+
* - we don't duplicate login UI in this Node server
|
|
14
|
+
* - the MCP server stays purely API-shaped
|
|
15
|
+
*/
|
|
16
|
+
export function handleAuthorize(req, res, cfg) {
|
|
17
|
+
if (!req.url)
|
|
18
|
+
return sendError(res, 400, "invalid_request", "Missing URL");
|
|
19
|
+
const incoming = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
|
|
20
|
+
const target = new URL(cfg.dashboardAuthorizeUrl);
|
|
21
|
+
// Forward all incoming query params (response_type, client_id, redirect_uri,
|
|
22
|
+
// state, scope, code_challenge, code_challenge_method, ...).
|
|
23
|
+
incoming.searchParams.forEach((value, key) => target.searchParams.set(key, value));
|
|
24
|
+
sendRedirect(res, target.toString());
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=authorize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/oauth/authorize.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,GAAmB,EACnB,GAAgB;IAEhB,IAAI,CAAC,GAAG,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,EAAE,aAAa,CAAC,CAAC;IAC3E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAClD,6EAA6E;IAC7E,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IACnF,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth configuration. Read once at startup from env vars.
|
|
3
|
+
*
|
|
4
|
+
* Required for OAuth to work:
|
|
5
|
+
* OAUTH_ISSUER = https://mcp.upload-post.com
|
|
6
|
+
* OAUTH_DASHBOARD_AUTHORIZE_URL = https://app.upload-post.com/oauth/authorize
|
|
7
|
+
* OAUTH_UPSTREAM_BASE_URL = https://api.upload-post.com (or staging equivalent)
|
|
8
|
+
* OAUTH_INTERNAL_SECRET = shared secret with the Upload-Post backend
|
|
9
|
+
*/
|
|
10
|
+
export interface OAuthConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
issuer: string;
|
|
13
|
+
dashboardAuthorizeUrl: string;
|
|
14
|
+
upstreamBaseUrl: string;
|
|
15
|
+
internalSecret: string;
|
|
16
|
+
introspectCacheTtlMs: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadOAuthConfig(): OAuthConfig;
|
|
19
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/oauth/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB,EAAE,MAAM,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,eAAe,IAAI,WAAW,CAkB7C"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth configuration. Read once at startup from env vars.
|
|
3
|
+
*
|
|
4
|
+
* Required for OAuth to work:
|
|
5
|
+
* OAUTH_ISSUER = https://mcp.upload-post.com
|
|
6
|
+
* OAUTH_DASHBOARD_AUTHORIZE_URL = https://app.upload-post.com/oauth/authorize
|
|
7
|
+
* OAUTH_UPSTREAM_BASE_URL = https://api.upload-post.com (or staging equivalent)
|
|
8
|
+
* OAUTH_INTERNAL_SECRET = shared secret with the Upload-Post backend
|
|
9
|
+
*/
|
|
10
|
+
export function loadOAuthConfig() {
|
|
11
|
+
const issuer = (process.env.OAUTH_ISSUER ?? "").replace(/\/$/, "");
|
|
12
|
+
const dashboardAuthorizeUrl = process.env.OAUTH_DASHBOARD_AUTHORIZE_URL ?? "";
|
|
13
|
+
const upstreamBaseUrl = (process.env.OAUTH_UPSTREAM_BASE_URL ?? "").replace(/\/$/, "");
|
|
14
|
+
const internalSecret = process.env.OAUTH_INTERNAL_SECRET ?? "";
|
|
15
|
+
const enabled = issuer.length > 0 &&
|
|
16
|
+
dashboardAuthorizeUrl.length > 0 &&
|
|
17
|
+
upstreamBaseUrl.length > 0 &&
|
|
18
|
+
internalSecret.length > 0;
|
|
19
|
+
return {
|
|
20
|
+
enabled,
|
|
21
|
+
issuer,
|
|
22
|
+
dashboardAuthorizeUrl,
|
|
23
|
+
upstreamBaseUrl,
|
|
24
|
+
internalSecret,
|
|
25
|
+
introspectCacheTtlMs: Number(process.env.OAUTH_INTROSPECT_CACHE_MS ?? 60_000),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/oauth/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,EAAE,CAAC;IAC9E,MAAM,eAAe,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvF,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC;IAC/D,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,GAAG,CAAC;QACjB,qBAAqB,CAAC,MAAM,GAAG,CAAC;QAChC,eAAe,CAAC,MAAM,GAAG,CAAC;QAC1B,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5B,OAAO;QACL,OAAO;QACP,MAAM;QACN,qBAAqB;QACrB,eAAe;QACf,cAAc;QACd,oBAAoB,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,MAAM,CAAC;KAC9E,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export declare function readBuffer(req: IncomingMessage): Promise<Buffer>;
|
|
3
|
+
export declare function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown> | null>;
|
|
4
|
+
export declare function readFormBody(req: IncomingMessage): Promise<Record<string, string>>;
|
|
5
|
+
export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
|
|
6
|
+
export declare function sendError(res: ServerResponse, status: number, error: string, description?: string): void;
|
|
7
|
+
export declare function sendRedirect(res: ServerResponse, url: string): void;
|
|
8
|
+
//# sourceMappingURL=http_utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http_utils.d.ts","sourceRoot":"","sources":["../../src/oauth/http_utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjE,wBAAsB,UAAU,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAUtE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAIhG;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOxF;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAKjF;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAIN;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAKnE"}
|