@orchyn/mcp 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -16
- package/dist/index.js +52 -34
- package/dist/video.js +15 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,16 +38,25 @@ npx @orchyn/mcp login # one-time sign-in (Google)
|
|
|
38
38
|
|
|
39
39
|
| Tool | Credits | Description |
|
|
40
40
|
|------|---------|-------------|
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
41
|
+
| `analyze_post` | first free* | **Preferred.** Analyze any post (video, image, carousel/slideshow) from a TikTok/Instagram/YouTube/X-Twitter URL — imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). Returns a `jobId` to poll via `GET /ai/analyze-post`. *First analysis free per workspace via the dashboard free grant.* |
|
|
42
|
+
| `analyze_video` | first free* | **Deprecated alias of `analyze_post`** — kept for backwards compatibility. Use `analyze_post` for new integrations. |
|
|
43
|
+
| `get_social_media` | 1 | Fetch a post's media from a URL: `contentType` (video/image/carousel/slideshow), title, caption, author, stats, direct media URLs **+ inline thumbnail image in chat**. |
|
|
44
|
+
| `discover_social_videos` | 2 | Find recent posts for a niche (YouTube via `yt-dlp` search; TikTok/Instagram via Apify). Returns up to 6 posts with stats **and inline thumbnails** — see *Images in chat* below. |
|
|
45
|
+
| `understand_social_post` | 10 | Import a post URL **and** analyze it with multimodal AI over the actual video/images: factual `whatHappens` description, hook strength, viral triggers, format breakdown, variation ideas, suggested hook/hashtags. Includes inline thumbnails. |
|
|
45
46
|
|
|
46
|
-
All tools require a connected orchyn account and are billed against your
|
|
47
|
-
orchyn credit balance (`POST /billing/mcp-credits/checkout` tops up).
|
|
47
|
+
All tools require a connected orchyn account and are billed against your orchyn credit balance (`POST /billing/mcp-credits/checkout` tops up).
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
`*` `analyze_post`/`analyze_video` bill against **workspace** credits (first free grant); the other three bill against **per-user MCP** credits (never tied to an app/workspace).
|
|
50
|
+
|
|
51
|
+
## Images in Claude / ChatGPT chat
|
|
52
|
+
|
|
53
|
+
Every tool that returns posts also renders **inline thumbnails** directly in the chat:
|
|
54
|
+
|
|
55
|
+
- `get_social_media` / `understand_social_post` / `analyze_post` — up to 4 frames inline (poster + carousel slides). The full `mediaItems[].preview_url` + `thumbnailUrl` stay in `structuredContent` for the model to reason over.
|
|
56
|
+
- `discover_social_videos` — each discovered post shows its thumbnail inline (up to 4 of the 6 results at once). Say **"next"** or **"show more"** — Claude will re-call `discover_social_videos` with a larger `limit` or paginate. Say **"analyze the 2nd one"** — Claude calls `analyze_post` or `understand_social_post` on that URL.
|
|
57
|
+
- **Batch analysis** — ask "analyze all 4" or "understand these 3 in batch" and Claude will call `analyze_post`/`understand_social_post` once per URL in parallel and summarize. For large batches, `discover_social_videos` + a follow-up `analyze_post` per URL is the recommended flow.
|
|
58
|
+
|
|
59
|
+
> The backend's `analyze_post` now watches the **actual video/images** (direct MP4, YouTube `fileUri`, or 6 carousel frames via Gemini multimodal) — not just the caption. The analysis includes a `whatHappens` field describing exactly what is seen.
|
|
51
60
|
|
|
52
61
|
## Prerequisites
|
|
53
62
|
|
|
@@ -229,15 +238,14 @@ per the MCP 2025-03-26 spec):
|
|
|
229
238
|
orchyn session (valid 1 hour)
|
|
230
239
|
- every MCP RPC validates the Bearer token against the session map
|
|
231
240
|
|
|
232
|
-
## Supported
|
|
241
|
+
## Supported URLs
|
|
233
242
|
|
|
234
243
|
- TikTok: `tiktok.com/*`, `vm.tiktok.com/*` (and `www.`/`m.` subdomains)
|
|
235
|
-
- Instagram: `instagram.com/*` (reels, posts), `instagr.am/*`
|
|
244
|
+
- Instagram: `instagram.com/*` (reels, posts, carousels), `instagr.am/*`
|
|
236
245
|
- YouTube: `youtube.com/*` (including `/shorts/`), `youtu.be/*`, `m.youtube.com/*`
|
|
237
|
-
- X/Twitter: `x.com/*`, `twitter.com/*`
|
|
238
|
-
`understand_social_post` (`analyze_video` covers TikTok/Instagram/YouTube only)
|
|
246
|
+
- X/Twitter: `x.com/*`, `twitter.com/*`
|
|
239
247
|
|
|
240
|
-
|
|
248
|
+
All tools accept these hosts. `analyze_post` (and its `analyze_video` alias) additionally handles **image, carousel and slideshow** posts — not just video.
|
|
241
249
|
|
|
242
250
|
## Troubleshooting
|
|
243
251
|
|
|
@@ -245,9 +253,9 @@ Other hosts are rejected by the tools.
|
|
|
245
253
|
`ORCHYN_ACCESS_TOKEN`.
|
|
246
254
|
- **402 paywall / `insufficient MCP credits`**: your orchyn account is out of
|
|
247
255
|
credits for this tool. Each call costs: `get_social_media` 1,
|
|
248
|
-
`discover_social_videos` 2, `understand_social_post` 10, `
|
|
249
|
-
first-call free. Top up via the orchyn dashboard
|
|
250
|
-
`POST /billing/mcp-credits/checkout`.
|
|
256
|
+
`discover_social_videos` 2, `understand_social_post` 10, `analyze_post`/
|
|
257
|
+
`analyze_video` first-call free* (see Tools). Top up via the orchyn dashboard
|
|
258
|
+
billing page or `POST /billing/mcp-credits/checkout`.
|
|
251
259
|
- **Expired refresh token**: the stored refresh token was rejected by the
|
|
252
260
|
orchyn server. Run `npx @orchyn/mcp login` again to re-authenticate.
|
|
253
261
|
- **`Could not reach the orchyn server`**: `ORCHYN_BASE_URL` is unreachable or
|
package/dist/index.js
CHANGED
|
@@ -18,18 +18,7 @@ import { getBaseUrl, getPublicUrl, getPort, getCredentialsFile, getTransportMode
|
|
|
18
18
|
import { AuthManager, OrchynAuthError, createHttpTokenProvider, createStdioTokenProvider } from "./auth.js";
|
|
19
19
|
import { OrchynClient, OrchynError } from "./orchyn.js";
|
|
20
20
|
import { OAuthManager } from "./oauth.js";
|
|
21
|
-
import { formatPaywallError, runVideoAnalysis,
|
|
22
|
-
const TOOL_NAME = "analyze_video";
|
|
23
|
-
const TOOL_DESCRIPTION = "Start an AI analysis of a TikTok, Instagram, or YouTube video from its link. " +
|
|
24
|
-
"Requires a connected orchyn account; consumes orchyn credits (first analysis free). " +
|
|
25
|
-
"Returns the analysis result once finished.";
|
|
26
|
-
const TOOL_INPUT_SCHEMA = z
|
|
27
|
-
.object({
|
|
28
|
-
url: z
|
|
29
|
-
.string()
|
|
30
|
-
.describe("Public video URL (tiktok.com, instagram.com, youtube.com, youtu.be, or common shortlinks)."),
|
|
31
|
-
})
|
|
32
|
-
.strict();
|
|
21
|
+
import { formatPaywallError, runVideoAnalysis, validatePostUrl } from "./video.js";
|
|
33
22
|
function toToolResult(proxy) {
|
|
34
23
|
const images = proxy.contentBlocks
|
|
35
24
|
.filter((c) => c.type === "image")
|
|
@@ -48,19 +37,25 @@ function toolError(prefix, err) {
|
|
|
48
37
|
export function createServer(opts) {
|
|
49
38
|
const server = new McpServer({
|
|
50
39
|
name: "orchyn-mcp",
|
|
51
|
-
version: "1.
|
|
40
|
+
version: "1.1.0",
|
|
52
41
|
});
|
|
53
|
-
server.registerTool(
|
|
54
|
-
title: "Analyze
|
|
55
|
-
description:
|
|
56
|
-
|
|
42
|
+
server.registerTool("analyze_post", {
|
|
43
|
+
title: "Analyze Post",
|
|
44
|
+
description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
|
|
45
|
+
"imports the media and runs AI analysis over the actual content (video frames, carousel images, caption). " +
|
|
46
|
+
"Supports TikTok, Instagram, YouTube and X/Twitter. Returns the full analysis once finished.",
|
|
47
|
+
inputSchema: z
|
|
48
|
+
.object({
|
|
49
|
+
url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X or shortlinks)."),
|
|
50
|
+
})
|
|
51
|
+
.strict(),
|
|
57
52
|
}, async (args, extra) => {
|
|
58
53
|
let session;
|
|
59
54
|
if (extra.authInfo?.token && opts.resolveSession) {
|
|
60
55
|
session = opts.resolveSession(extra.authInfo.token);
|
|
61
56
|
}
|
|
62
57
|
const client = opts.makeClient(session);
|
|
63
|
-
const validation =
|
|
58
|
+
const validation = validatePostUrl(args.url);
|
|
64
59
|
if (!validation.ok) {
|
|
65
60
|
return {
|
|
66
61
|
content: [{ type: "text", text: `Invalid url: ${validation.error}` }],
|
|
@@ -69,33 +64,54 @@ export function createServer(opts) {
|
|
|
69
64
|
}
|
|
70
65
|
try {
|
|
71
66
|
const result = await runVideoAnalysis(client, validation.url);
|
|
72
|
-
return {
|
|
73
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
74
|
-
};
|
|
67
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
75
68
|
}
|
|
76
69
|
catch (err) {
|
|
77
70
|
if (err instanceof OrchynError && err.paywall) {
|
|
78
71
|
return {
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: "text",
|
|
82
|
-
text: `Analysis blocked: ${formatPaywallError(err)}\n\nHTTP ${err.status}: ${err.message}`,
|
|
83
|
-
},
|
|
84
|
-
],
|
|
72
|
+
content: [{ type: "text", text: `Analysis blocked: ${formatPaywallError(err)}\n\nHTTP ${err.status}: ${err.message}` }],
|
|
85
73
|
isError: true,
|
|
86
74
|
};
|
|
87
75
|
}
|
|
88
76
|
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { content: [{ type: "text", text: `Analysis failed: ${msg}` }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
server.registerTool("analyze_video", {
|
|
81
|
+
title: "Analyze Video",
|
|
82
|
+
description: "[Deprecated — use analyze_post] Alias of analyze_post — analyzes any post type.",
|
|
83
|
+
inputSchema: z
|
|
84
|
+
.object({
|
|
85
|
+
url: z.string().describe("Public post URL (TikTok/Instagram/YouTube/X or shortlinks)."),
|
|
86
|
+
})
|
|
87
|
+
.strict(),
|
|
88
|
+
}, async (args, extra) => {
|
|
89
|
+
let session;
|
|
90
|
+
if (extra.authInfo?.token && opts.resolveSession) {
|
|
91
|
+
session = opts.resolveSession(extra.authInfo.token);
|
|
92
|
+
}
|
|
93
|
+
const client = opts.makeClient(session);
|
|
94
|
+
const validation = validatePostUrl(args.url);
|
|
95
|
+
if (!validation.ok) {
|
|
89
96
|
return {
|
|
90
|
-
content: [
|
|
91
|
-
{
|
|
92
|
-
type: "text",
|
|
93
|
-
text: `Video analysis failed: ${msg}`,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
97
|
+
content: [{ type: "text", text: `Invalid url: ${validation.error}` }],
|
|
96
98
|
isError: true,
|
|
97
99
|
};
|
|
98
100
|
}
|
|
101
|
+
try {
|
|
102
|
+
const result = await runVideoAnalysis(client, validation.url);
|
|
103
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof OrchynError && err.paywall) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Analysis blocked: ${formatPaywallError(err)}\n\nHTTP ${err.status}: ${err.message}` }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return { content: [{ type: "text", text: `Analysis failed: ${msg}` }], isError: true };
|
|
114
|
+
}
|
|
99
115
|
});
|
|
100
116
|
server.registerTool("get_social_media", {
|
|
101
117
|
title: "Get Social Media",
|
|
@@ -119,12 +135,14 @@ export function createServer(opts) {
|
|
|
119
135
|
server.registerTool("discover_social_videos", {
|
|
120
136
|
title: "Discover Social Videos",
|
|
121
137
|
description: "Discover recent videos/posts for a niche. YouTube via search; TikTok & Instagram via Apify. " +
|
|
122
|
-
"
|
|
138
|
+
"Returns inline thumbnails (up to 4) so they show in chat. Say \"next\" to paginate (offset), " +
|
|
139
|
+
"or \"analyze the 2nd one\" / \"analyze all\" for batch analysis. Consumes 2 orchyn credits.",
|
|
123
140
|
inputSchema: z
|
|
124
141
|
.object({
|
|
125
142
|
niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
|
|
126
143
|
keywords: z.string().optional().describe("Optional extra keywords."),
|
|
127
144
|
limit: z.number().int().optional().describe("Max results (default 6)."),
|
|
145
|
+
offset: z.number().int().optional().describe("Skip first N results — for 'next' pagination."),
|
|
128
146
|
platform: z
|
|
129
147
|
.enum(["youtube", "tiktok", "instagram", "any"])
|
|
130
148
|
.optional()
|
package/dist/video.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const POLL_INTERVAL_MS = 2000;
|
|
6
6
|
export const POLL_TIMEOUT_MS = 300_000;
|
|
7
|
-
const
|
|
7
|
+
const SUPPORTED_HOSTS_VIDEO = new Set([
|
|
8
8
|
"tiktok.com",
|
|
9
9
|
"vm.tiktok.com",
|
|
10
10
|
"instagram.com",
|
|
@@ -15,7 +15,13 @@ const SUPPORTED_HOSTS = new Set([
|
|
|
15
15
|
"youtube-nocookie.com",
|
|
16
16
|
"m.tiktok.com",
|
|
17
17
|
]);
|
|
18
|
-
|
|
18
|
+
const SUPPORTED_HOSTS_POST = new Set([
|
|
19
|
+
...SUPPORTED_HOSTS_VIDEO,
|
|
20
|
+
"x.com",
|
|
21
|
+
"twitter.com",
|
|
22
|
+
"mobile.twitter.com",
|
|
23
|
+
]);
|
|
24
|
+
function validateUrl(rawUrl, allowed) {
|
|
19
25
|
if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
|
|
20
26
|
return { ok: false, error: "url must be a non-empty string." };
|
|
21
27
|
}
|
|
@@ -36,7 +42,7 @@ export function validateVideoUrl(rawUrl) {
|
|
|
36
42
|
// accept all youtube.com paths, incl. /shorts/<id>
|
|
37
43
|
return { ok: true, url: parsed.toString() };
|
|
38
44
|
}
|
|
39
|
-
if (
|
|
45
|
+
if (allowed.has(host)) {
|
|
40
46
|
return { ok: true, url: parsed.toString() };
|
|
41
47
|
}
|
|
42
48
|
return {
|
|
@@ -44,6 +50,12 @@ export function validateVideoUrl(rawUrl) {
|
|
|
44
50
|
error: "url host is not supported. Supported: tiktok.com, vm.tiktok.com, instagram.com, instagr.am, youtube.com, youtu.be, m.youtube.com (and /shorts).",
|
|
45
51
|
};
|
|
46
52
|
}
|
|
53
|
+
export function validateVideoUrl(rawUrl) {
|
|
54
|
+
return validateUrl(rawUrl, SUPPORTED_HOSTS_VIDEO);
|
|
55
|
+
}
|
|
56
|
+
export function validatePostUrl(rawUrl) {
|
|
57
|
+
return validateUrl(rawUrl, SUPPORTED_HOSTS_POST);
|
|
58
|
+
}
|
|
47
59
|
export class JobTimeoutError extends Error {
|
|
48
60
|
constructor(jobId, elapsedMs, lastStatus) {
|
|
49
61
|
super(`Timed out after ${Math.round(elapsedMs / 1000)}s waiting for analysis job ${jobId} to finish.` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchyn/mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "MCP server for orchyn - fetch, discover and understand TikTok/Instagram/YouTube/X posts with AI (media metadata, niche discovery, hook & viral analysis)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|