@orchyn/mcp 1.10.0 → 1.12.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/shared/tools.js +157 -7
- package/package.json +1 -1
package/dist/shared/tools.js
CHANGED
|
@@ -10,12 +10,59 @@ import { z } from "zod";
|
|
|
10
10
|
import { OrchynError } from "./orchyn.js";
|
|
11
11
|
import { formatPaywallError, runVideoAnalysis, validatePostUrl } from "./video.js";
|
|
12
12
|
import { ORCHYN_UI_TEMPLATE } from "./ui-template.js";
|
|
13
|
+
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
14
|
+
export const MCP_SERVER_VERSION = "1.11.0";
|
|
13
15
|
/** MCP Apps extension identifier */
|
|
14
16
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
15
17
|
/** MIME type for MCP Apps HTML resources */
|
|
16
18
|
const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
17
19
|
/** Single UI resource URI shared by all tools */
|
|
18
20
|
const UI_RESOURCE_URI = "ui://orchyn/view";
|
|
21
|
+
/**
|
|
22
|
+
* Rewrite external image URLs to go through the orchyn proxy so they
|
|
23
|
+
* work inside ChatGPT's sandboxed iframe (CORS + CSP restrictions).
|
|
24
|
+
*/
|
|
25
|
+
function proxyImageUrl(url) {
|
|
26
|
+
if (!url || url.startsWith("data:"))
|
|
27
|
+
return url;
|
|
28
|
+
try {
|
|
29
|
+
const u = new URL(url);
|
|
30
|
+
// Only proxy external HTTP(S) URLs — skip our own proxy and data URIs
|
|
31
|
+
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
32
|
+
const serverUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL || "";
|
|
33
|
+
if (serverUrl && !url.startsWith(serverUrl)) {
|
|
34
|
+
return `${serverUrl.replace(/\/+$/, "")}/media/proxy?url=${encodeURIComponent(url)}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
return url;
|
|
40
|
+
}
|
|
41
|
+
/** Recursively rewrite URL fields in a structured object for the proxy. */
|
|
42
|
+
function proxyUrls(obj) {
|
|
43
|
+
if (typeof obj === "string") {
|
|
44
|
+
// Check if it looks like a URL
|
|
45
|
+
if (/^https?:\/\//.test(obj) && !obj.includes("/media/proxy")) {
|
|
46
|
+
return proxyImageUrl(obj);
|
|
47
|
+
}
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(obj))
|
|
51
|
+
return obj.map(proxyUrls);
|
|
52
|
+
if (obj && typeof obj === "object") {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
55
|
+
if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "url"].includes(k) && typeof v === "string") {
|
|
56
|
+
out[k] = proxyImageUrl(v);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
out[k] = proxyUrls(v);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return obj;
|
|
65
|
+
}
|
|
19
66
|
function toToolResult(proxy) {
|
|
20
67
|
const images = proxy.contentBlocks
|
|
21
68
|
.filter((c) => c.type === "image")
|
|
@@ -33,19 +80,110 @@ function toToolResult(proxy) {
|
|
|
33
80
|
? rawText.substring(0, rawText.indexOf("\n\n{")).trimEnd()
|
|
34
81
|
: "";
|
|
35
82
|
const structured = proxy.structured;
|
|
36
|
-
|
|
37
|
-
const
|
|
83
|
+
// Proxy thumbnail URLs in structured content for ChatGPT iframe
|
|
84
|
+
const proxied = structured ? proxyUrls(structured) : {};
|
|
85
|
+
const textJson = JSON.stringify(proxied ?? {}, null, 2);
|
|
86
|
+
// Replace image URLs in HTML with proxied versions
|
|
87
|
+
const proxiedHtml = htmlPrefix ? proxyImageUrlsInHtml(htmlPrefix) : "";
|
|
88
|
+
const text = proxiedHtml ? `${proxiedHtml}\n\n${textJson}` : textJson;
|
|
38
89
|
return {
|
|
39
90
|
content: [...images, { type: "text", text }],
|
|
40
|
-
structuredContent:
|
|
91
|
+
structuredContent: proxied ?? {},
|
|
41
92
|
};
|
|
42
93
|
}
|
|
94
|
+
/** Replace external image src/href URLs in HTML with proxied versions. */
|
|
95
|
+
function proxyImageUrlsInHtml(html) {
|
|
96
|
+
return html.replace(/(src|href)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
|
|
97
|
+
return `${attr}="${proxyImageUrl(url)}"`;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
43
100
|
function toolError(prefix, err) {
|
|
44
101
|
const msg = err instanceof Error ? err.message : String(err);
|
|
45
102
|
return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
|
|
46
103
|
}
|
|
104
|
+
/** Build an HTML card for analyze_post results. */
|
|
105
|
+
function buildAnalysisHtmlCard(result, thumbnailUrl, platform, creatorHandle, title) {
|
|
106
|
+
const platformEmoji = {
|
|
107
|
+
tiktok: "🎵", instagram: "📸", youtube: "▶️", twitter: "🐦",
|
|
108
|
+
x: "🐦", douyin: "🎵", xiaohongshu: "📕", bilibili: "📺",
|
|
109
|
+
};
|
|
110
|
+
const platformColor = {
|
|
111
|
+
tiktok: "#000000", instagram: "#E4405F", youtube: "#FF0000",
|
|
112
|
+
twitter: "#1DA1F2", x: "#000000", douyin: "#000000",
|
|
113
|
+
xiaohongshu: "#FF2442", bilibili: "#00A1D6",
|
|
114
|
+
};
|
|
115
|
+
const emoji = platformEmoji[platform] || "📱";
|
|
116
|
+
const color = platformColor[platform] || "#6b7280";
|
|
117
|
+
const post = (result.post ?? {});
|
|
118
|
+
const analysis = (result.analysis ?? {});
|
|
119
|
+
const views = typeof post.views === "number" ? post.views.toLocaleString() : "—";
|
|
120
|
+
const likes = typeof post.likes === "number" ? post.likes.toLocaleString() : "—";
|
|
121
|
+
const comments = typeof post.comments === "number" ? post.comments.toLocaleString() : "—";
|
|
122
|
+
const shares = typeof post.shares === "number" ? post.shares.toLocaleString() : "—";
|
|
123
|
+
const hookStrength = typeof analysis.hookStrength === "number" ? analysis.hookStrength : null;
|
|
124
|
+
const summary = typeof analysis.summary === "string" ? analysis.summary : "";
|
|
125
|
+
const whyItWorks = typeof analysis.whyItWorks === "string" ? analysis.whyItWorks : "";
|
|
126
|
+
const viralTriggers = Array.isArray(analysis.viralTriggers) ? analysis.viralTriggers : [];
|
|
127
|
+
const variationIdeas = Array.isArray(analysis.variationIdeas) ? analysis.variationIdeas.slice(0, 3) : [];
|
|
128
|
+
const suggestedHook = typeof analysis.suggestedHook === "string" ? analysis.suggestedHook : "";
|
|
129
|
+
let html = `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;background:#0d1117;color:#e6edf3;border-radius:12px;overflow:hidden;margin:0 auto">`;
|
|
130
|
+
// Thumbnail
|
|
131
|
+
if (thumbnailUrl) {
|
|
132
|
+
html += `<div style="position:relative"><img src="${thumbnailUrl}" style="width:100%;max-height:400px;object-fit:cover;display:block" onerror="this.style.display='none'" /><div style="position:absolute;bottom:12px;left:12px;display:flex;gap:8px;flex-wrap:wrap">`;
|
|
133
|
+
html += `<span style="background:${color};color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600">${emoji} ${platform}</span>`;
|
|
134
|
+
html += `</div></div>`;
|
|
135
|
+
}
|
|
136
|
+
// Header
|
|
137
|
+
html += `<div style="padding:16px 20px">`;
|
|
138
|
+
html += `<div style="font-size:18px;font-weight:700;margin-bottom:4px">📊 AI Post Analysis</div>`;
|
|
139
|
+
if (creatorHandle)
|
|
140
|
+
html += `<div style="color:#8b949e;font-size:13px">@${creatorHandle}</div>`;
|
|
141
|
+
if (title)
|
|
142
|
+
html += `<div style="color:#c9d1d9;font-size:14px;margin-top:8px;line-height:1.4">${title.slice(0, 150)}</div>`;
|
|
143
|
+
// Stats
|
|
144
|
+
html += `<div style="display:flex;gap:12px;margin-top:12px;flex-wrap:wrap">`;
|
|
145
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">👁️ ${views}</span>`;
|
|
146
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">❤️ ${likes}</span>`;
|
|
147
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">💬 ${comments}</span>`;
|
|
148
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">🔄 ${shares}</span>`;
|
|
149
|
+
html += `</div>`;
|
|
150
|
+
// Hook strength bar
|
|
151
|
+
if (hookStrength !== null) {
|
|
152
|
+
const pct = Math.min(hookStrength * 10, 100);
|
|
153
|
+
const barColor = hookStrength >= 7 ? "#3fb950" : hookStrength >= 4 ? "#d29922" : "#f85149";
|
|
154
|
+
html += `<div style="margin-top:12px"><div style="font-size:12px;color:#8b949e;margin-bottom:4px">Hook Strength: ${hookStrength}/10</div><div style="background:#21262d;border-radius:999px;height:8px"><div style="background:${barColor};width:${pct}%;height:100%;border-radius:999px"></div></div></div>`;
|
|
155
|
+
}
|
|
156
|
+
// Summary
|
|
157
|
+
if (summary) {
|
|
158
|
+
html += `<div style="margin-top:14px;padding:12px;background:#161b22;border-radius:8px;border-left:3px solid ${color}"><div style="font-size:12px;font-weight:600;color:#8b949e;margin-bottom:4px">📝 Summary</div><div style="font-size:13px;line-height:1.5">${summary}</div></div>`;
|
|
159
|
+
}
|
|
160
|
+
// Why it works
|
|
161
|
+
if (whyItWorks) {
|
|
162
|
+
html += `<div style="margin-top:10px;padding:12px;background:#161b22;border-radius:8px;border-left:3px solid #3fb950"><div style="font-size:12px;font-weight:600;color:#8b949e;margin-bottom:4px">✅ Why It Works</div><div style="font-size:13px;line-height:1.5">${whyItWorks}</div></div>`;
|
|
163
|
+
}
|
|
164
|
+
// Viral triggers
|
|
165
|
+
if (viralTriggers.length > 0) {
|
|
166
|
+
html += `<div style="margin-top:10px"><div style="font-size:12px;font-weight:600;color:#8b949e;margin-bottom:6px">🔥 Viral Triggers</div><div style="display:flex;flex-wrap:wrap;gap:6px">`;
|
|
167
|
+
viralTriggers.forEach((t) => {
|
|
168
|
+
html += `<span style="background:#1f6feb33;color:#58a6ff;padding:4px 10px;border-radius:999px;font-size:12px">${t}</span>`;
|
|
169
|
+
});
|
|
170
|
+
html += `</div></div>`;
|
|
171
|
+
}
|
|
172
|
+
// Suggested hook
|
|
173
|
+
if (suggestedHook) {
|
|
174
|
+
html += `<div style="margin-top:10px;padding:12px;background:#161b22;border-radius:8px;border-left:3px solid #d29922"><div style="font-size:12px;font-weight:600;color:#8b949e;margin-bottom:4px">💡 Suggested Hook</div><div style="font-size:13px;font-style:italic;line-height:1.5">${suggestedHook}</div></div>`;
|
|
175
|
+
}
|
|
176
|
+
// Variation ideas
|
|
177
|
+
if (variationIdeas.length > 0) {
|
|
178
|
+
html += `<div style="margin-top:10px"><div style="font-size:12px;font-weight:600;color:#8b949e;margin-bottom:6px">🔄 Variation Ideas</div><ol style="margin:0;padding-left:18px;font-size:13px;line-height:1.6">`;
|
|
179
|
+
variationIdeas.forEach((v) => { html += `<li>${v}</li>`; });
|
|
180
|
+
html += `</ol></div>`;
|
|
181
|
+
}
|
|
182
|
+
html += `</div></div>`;
|
|
183
|
+
return html;
|
|
184
|
+
}
|
|
47
185
|
export function createMcpServer(makeClient) {
|
|
48
|
-
const server = new McpServer({ name: "orchyn-mcp", version:
|
|
186
|
+
const server = new McpServer({ name: "orchyn-mcp", version: MCP_SERVER_VERSION }, {
|
|
49
187
|
capabilities: {
|
|
50
188
|
resources: {},
|
|
51
189
|
extensions: {
|
|
@@ -107,15 +245,27 @@ export function createMcpServer(makeClient) {
|
|
|
107
245
|
}
|
|
108
246
|
try {
|
|
109
247
|
const result = await runVideoAnalysis(client, validation.url);
|
|
248
|
+
// Remove internal field from the JSON shown to the model
|
|
249
|
+
const { inlineImages: _omit, ...rest } = result;
|
|
250
|
+
// Proxy thumbnail URLs in structured content for ChatGPT iframe
|
|
251
|
+
const proxied = proxyUrls(rest);
|
|
252
|
+
// Build HTML card for interactive UI
|
|
253
|
+
const post = (rest.post ?? {});
|
|
254
|
+
const analysis = (rest.analysis ?? {});
|
|
255
|
+
const thumbnailUrl = typeof post.thumbnailUrl === "string" ? proxyImageUrl(post.thumbnailUrl) : "";
|
|
256
|
+
const platform = String(post.platform ?? rest.platform ?? "");
|
|
257
|
+
const creatorHandle = String(post.creatorHandle ?? "");
|
|
258
|
+
const title = String(post.title ?? post.caption ?? "");
|
|
259
|
+
const htmlCard = buildAnalysisHtmlCard(proxied, thumbnailUrl, platform, creatorHandle, title);
|
|
110
260
|
const images = (result.inlineImages ?? []).map((img) => ({
|
|
111
261
|
type: "image",
|
|
112
262
|
data: String(img.data),
|
|
113
263
|
mimeType: String(img.mimeType ?? "image/jpeg"),
|
|
114
264
|
}));
|
|
115
|
-
|
|
116
|
-
const { inlineImages: _omit, ...rest } = result;
|
|
265
|
+
const textJson = JSON.stringify(proxied, null, 2);
|
|
117
266
|
return {
|
|
118
|
-
content: [...images, { type: "text", text:
|
|
267
|
+
content: [...images, { type: "text", text: `${htmlCard}\n\n${textJson}` }],
|
|
268
|
+
structuredContent: proxied,
|
|
119
269
|
};
|
|
120
270
|
}
|
|
121
271
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchyn/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
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",
|