@orchyn/mcp 1.9.1 → 1.11.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 +183 -19
- package/package.json +1 -1
package/dist/shared/tools.js
CHANGED
|
@@ -16,6 +16,51 @@ const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
|
16
16
|
const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
17
17
|
/** Single UI resource URI shared by all tools */
|
|
18
18
|
const UI_RESOURCE_URI = "ui://orchyn/view";
|
|
19
|
+
/**
|
|
20
|
+
* Rewrite external image URLs to go through the orchyn proxy so they
|
|
21
|
+
* work inside ChatGPT's sandboxed iframe (CORS + CSP restrictions).
|
|
22
|
+
*/
|
|
23
|
+
function proxyImageUrl(url) {
|
|
24
|
+
if (!url || url.startsWith("data:"))
|
|
25
|
+
return url;
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(url);
|
|
28
|
+
// Only proxy external HTTP(S) URLs — skip our own proxy and data URIs
|
|
29
|
+
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
30
|
+
const serverUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL || "";
|
|
31
|
+
if (serverUrl && !url.startsWith(serverUrl)) {
|
|
32
|
+
return `${serverUrl.replace(/\/+$/, "")}/media/proxy?url=${encodeURIComponent(url)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
/** Recursively rewrite URL fields in a structured object for the proxy. */
|
|
40
|
+
function proxyUrls(obj) {
|
|
41
|
+
if (typeof obj === "string") {
|
|
42
|
+
// Check if it looks like a URL
|
|
43
|
+
if (/^https?:\/\//.test(obj) && !obj.includes("/media/proxy")) {
|
|
44
|
+
return proxyImageUrl(obj);
|
|
45
|
+
}
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(obj))
|
|
49
|
+
return obj.map(proxyUrls);
|
|
50
|
+
if (obj && typeof obj === "object") {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
53
|
+
if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "url"].includes(k) && typeof v === "string") {
|
|
54
|
+
out[k] = proxyImageUrl(v);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
out[k] = proxyUrls(v);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
return obj;
|
|
63
|
+
}
|
|
19
64
|
function toToolResult(proxy) {
|
|
20
65
|
const images = proxy.contentBlocks
|
|
21
66
|
.filter((c) => c.type === "image")
|
|
@@ -33,14 +78,108 @@ function toToolResult(proxy) {
|
|
|
33
78
|
? rawText.substring(0, rawText.indexOf("\n\n{")).trimEnd()
|
|
34
79
|
: "";
|
|
35
80
|
const structured = proxy.structured;
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
81
|
+
// Proxy thumbnail URLs in structured content for ChatGPT iframe
|
|
82
|
+
const proxied = structured ? proxyUrls(structured) : {};
|
|
83
|
+
const textJson = JSON.stringify(proxied ?? {}, null, 2);
|
|
84
|
+
// Replace image URLs in HTML with proxied versions
|
|
85
|
+
const proxiedHtml = htmlPrefix ? proxyImageUrlsInHtml(htmlPrefix) : "";
|
|
86
|
+
const text = proxiedHtml ? `${proxiedHtml}\n\n${textJson}` : textJson;
|
|
87
|
+
return {
|
|
88
|
+
content: [...images, { type: "text", text }],
|
|
89
|
+
structuredContent: proxied ?? {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Replace external image src/href URLs in HTML with proxied versions. */
|
|
93
|
+
function proxyImageUrlsInHtml(html) {
|
|
94
|
+
return html.replace(/(src|href)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
|
|
95
|
+
return `${attr}="${proxyImageUrl(url)}"`;
|
|
96
|
+
});
|
|
39
97
|
}
|
|
40
98
|
function toolError(prefix, err) {
|
|
41
99
|
const msg = err instanceof Error ? err.message : String(err);
|
|
42
100
|
return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
|
|
43
101
|
}
|
|
102
|
+
/** Build an HTML card for analyze_post results. */
|
|
103
|
+
function buildAnalysisHtmlCard(result, thumbnailUrl, platform, creatorHandle, title) {
|
|
104
|
+
const platformEmoji = {
|
|
105
|
+
tiktok: "🎵", instagram: "📸", youtube: "▶️", twitter: "🐦",
|
|
106
|
+
x: "🐦", douyin: "🎵", xiaohongshu: "📕", bilibili: "📺",
|
|
107
|
+
};
|
|
108
|
+
const platformColor = {
|
|
109
|
+
tiktok: "#000000", instagram: "#E4405F", youtube: "#FF0000",
|
|
110
|
+
twitter: "#1DA1F2", x: "#000000", douyin: "#000000",
|
|
111
|
+
xiaohongshu: "#FF2442", bilibili: "#00A1D6",
|
|
112
|
+
};
|
|
113
|
+
const emoji = platformEmoji[platform] || "📱";
|
|
114
|
+
const color = platformColor[platform] || "#6b7280";
|
|
115
|
+
const post = (result.post ?? {});
|
|
116
|
+
const analysis = (result.analysis ?? {});
|
|
117
|
+
const views = typeof post.views === "number" ? post.views.toLocaleString() : "—";
|
|
118
|
+
const likes = typeof post.likes === "number" ? post.likes.toLocaleString() : "—";
|
|
119
|
+
const comments = typeof post.comments === "number" ? post.comments.toLocaleString() : "—";
|
|
120
|
+
const shares = typeof post.shares === "number" ? post.shares.toLocaleString() : "—";
|
|
121
|
+
const hookStrength = typeof analysis.hookStrength === "number" ? analysis.hookStrength : null;
|
|
122
|
+
const summary = typeof analysis.summary === "string" ? analysis.summary : "";
|
|
123
|
+
const whyItWorks = typeof analysis.whyItWorks === "string" ? analysis.whyItWorks : "";
|
|
124
|
+
const viralTriggers = Array.isArray(analysis.viralTriggers) ? analysis.viralTriggers : [];
|
|
125
|
+
const variationIdeas = Array.isArray(analysis.variationIdeas) ? analysis.variationIdeas.slice(0, 3) : [];
|
|
126
|
+
const suggestedHook = typeof analysis.suggestedHook === "string" ? analysis.suggestedHook : "";
|
|
127
|
+
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">`;
|
|
128
|
+
// Thumbnail
|
|
129
|
+
if (thumbnailUrl) {
|
|
130
|
+
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">`;
|
|
131
|
+
html += `<span style="background:${color};color:#fff;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600">${emoji} ${platform}</span>`;
|
|
132
|
+
html += `</div></div>`;
|
|
133
|
+
}
|
|
134
|
+
// Header
|
|
135
|
+
html += `<div style="padding:16px 20px">`;
|
|
136
|
+
html += `<div style="font-size:18px;font-weight:700;margin-bottom:4px">📊 AI Post Analysis</div>`;
|
|
137
|
+
if (creatorHandle)
|
|
138
|
+
html += `<div style="color:#8b949e;font-size:13px">@${creatorHandle}</div>`;
|
|
139
|
+
if (title)
|
|
140
|
+
html += `<div style="color:#c9d1d9;font-size:14px;margin-top:8px;line-height:1.4">${title.slice(0, 150)}</div>`;
|
|
141
|
+
// Stats
|
|
142
|
+
html += `<div style="display:flex;gap:12px;margin-top:12px;flex-wrap:wrap">`;
|
|
143
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">👁️ ${views}</span>`;
|
|
144
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">❤️ ${likes}</span>`;
|
|
145
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">💬 ${comments}</span>`;
|
|
146
|
+
html += `<span style="background:#21262d;padding:4px 10px;border-radius:8px;font-size:13px">🔄 ${shares}</span>`;
|
|
147
|
+
html += `</div>`;
|
|
148
|
+
// Hook strength bar
|
|
149
|
+
if (hookStrength !== null) {
|
|
150
|
+
const pct = Math.min(hookStrength * 10, 100);
|
|
151
|
+
const barColor = hookStrength >= 7 ? "#3fb950" : hookStrength >= 4 ? "#d29922" : "#f85149";
|
|
152
|
+
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>`;
|
|
153
|
+
}
|
|
154
|
+
// Summary
|
|
155
|
+
if (summary) {
|
|
156
|
+
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>`;
|
|
157
|
+
}
|
|
158
|
+
// Why it works
|
|
159
|
+
if (whyItWorks) {
|
|
160
|
+
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>`;
|
|
161
|
+
}
|
|
162
|
+
// Viral triggers
|
|
163
|
+
if (viralTriggers.length > 0) {
|
|
164
|
+
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">`;
|
|
165
|
+
viralTriggers.forEach((t) => {
|
|
166
|
+
html += `<span style="background:#1f6feb33;color:#58a6ff;padding:4px 10px;border-radius:999px;font-size:12px">${t}</span>`;
|
|
167
|
+
});
|
|
168
|
+
html += `</div></div>`;
|
|
169
|
+
}
|
|
170
|
+
// Suggested hook
|
|
171
|
+
if (suggestedHook) {
|
|
172
|
+
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>`;
|
|
173
|
+
}
|
|
174
|
+
// Variation ideas
|
|
175
|
+
if (variationIdeas.length > 0) {
|
|
176
|
+
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">`;
|
|
177
|
+
variationIdeas.forEach((v) => { html += `<li>${v}</li>`; });
|
|
178
|
+
html += `</ol></div>`;
|
|
179
|
+
}
|
|
180
|
+
html += `</div></div>`;
|
|
181
|
+
return html;
|
|
182
|
+
}
|
|
44
183
|
export function createMcpServer(makeClient) {
|
|
45
184
|
const server = new McpServer({ name: "orchyn-mcp", version: "1.8.0" }, {
|
|
46
185
|
capabilities: {
|
|
@@ -53,22 +192,35 @@ export function createMcpServer(makeClient) {
|
|
|
53
192
|
// Register the single UI resource that all tools share.
|
|
54
193
|
// The host reads this resource, renders it in a sandboxed iframe,
|
|
55
194
|
// and pushes tool results via ui/notifications/tool-result.
|
|
56
|
-
server.registerResource("Orchyn Interactive View", UI_RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async () =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
195
|
+
server.registerResource("Orchyn Interactive View", UI_RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => {
|
|
196
|
+
// Build CSP resourceDomains from env so proxied thumbnails work
|
|
197
|
+
const domains = [
|
|
198
|
+
"https://*.tiktokcdn.com",
|
|
199
|
+
"https://*.cdninstagram.com",
|
|
200
|
+
"https://*.ytimg.com",
|
|
201
|
+
"https://*.googlevideo.com",
|
|
202
|
+
];
|
|
203
|
+
const apiUrl = process.env.ORCHYN_API_URL || process.env.ORCHYN_BASE_URL;
|
|
204
|
+
if (apiUrl && apiUrl.trim()) {
|
|
205
|
+
domains.push(apiUrl.trim().replace(/\/+$/, ""));
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
contents: [
|
|
209
|
+
{
|
|
210
|
+
uri: UI_RESOURCE_URI,
|
|
211
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
212
|
+
text: ORCHYN_UI_TEMPLATE,
|
|
213
|
+
_meta: {
|
|
214
|
+
ui: {
|
|
215
|
+
csp: {
|
|
216
|
+
resourceDomains: domains,
|
|
217
|
+
},
|
|
66
218
|
},
|
|
67
219
|
},
|
|
68
220
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
});
|
|
72
224
|
server.registerTool("analyze_post", {
|
|
73
225
|
title: "Analyze Post",
|
|
74
226
|
description: "Analyze a social post (video, image, carousel/slideshow) from its link — " +
|
|
@@ -91,15 +243,27 @@ export function createMcpServer(makeClient) {
|
|
|
91
243
|
}
|
|
92
244
|
try {
|
|
93
245
|
const result = await runVideoAnalysis(client, validation.url);
|
|
246
|
+
// Remove internal field from the JSON shown to the model
|
|
247
|
+
const { inlineImages: _omit, ...rest } = result;
|
|
248
|
+
// Proxy thumbnail URLs in structured content for ChatGPT iframe
|
|
249
|
+
const proxied = proxyUrls(rest);
|
|
250
|
+
// Build HTML card for interactive UI
|
|
251
|
+
const post = (rest.post ?? {});
|
|
252
|
+
const analysis = (rest.analysis ?? {});
|
|
253
|
+
const thumbnailUrl = typeof post.thumbnailUrl === "string" ? proxyImageUrl(post.thumbnailUrl) : "";
|
|
254
|
+
const platform = String(post.platform ?? rest.platform ?? "");
|
|
255
|
+
const creatorHandle = String(post.creatorHandle ?? "");
|
|
256
|
+
const title = String(post.title ?? post.caption ?? "");
|
|
257
|
+
const htmlCard = buildAnalysisHtmlCard(proxied, thumbnailUrl, platform, creatorHandle, title);
|
|
94
258
|
const images = (result.inlineImages ?? []).map((img) => ({
|
|
95
259
|
type: "image",
|
|
96
260
|
data: String(img.data),
|
|
97
261
|
mimeType: String(img.mimeType ?? "image/jpeg"),
|
|
98
262
|
}));
|
|
99
|
-
|
|
100
|
-
const { inlineImages: _omit, ...rest } = result;
|
|
263
|
+
const textJson = JSON.stringify(proxied, null, 2);
|
|
101
264
|
return {
|
|
102
|
-
content: [...images, { type: "text", text:
|
|
265
|
+
content: [...images, { type: "text", text: `${htmlCard}\n\n${textJson}` }],
|
|
266
|
+
structuredContent: proxied,
|
|
103
267
|
};
|
|
104
268
|
}
|
|
105
269
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchyn/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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",
|