forkfeed-mcp 1.0.12 → 1.0.13

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.
Files changed (2) hide show
  1. package/dist/index.js +98 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,6 +9,86 @@ import { GUIDE_CONTENT } from './guide-content.js';
9
9
  import { IMAGE_CATALOG } from './image-catalog.js';
10
10
  const APP_SERVER_URL = (process.env.APP_SERVER_URL || 'https://api.forkfeed.link').replace(/\/+$/, '');
11
11
  const TOKEN = process.env.FORKFEED_TOKEN || '';
12
+ // ── Manifest schemas ──────────────────────────────────────────────────
13
+ const forkSchema = z.object({
14
+ _id: z.string().min(1, 'Fork _id is required'),
15
+ title: z.string().min(1, 'Fork title is required'),
16
+ description: z.string(),
17
+ imageSrc: z.string(),
18
+ feedIds: z.array(z.string().min(1)).min(1, 'Fork must reference at least one feed'),
19
+ actionLabel: z.string().optional(),
20
+ actionUrl: z.string().optional(),
21
+ }).passthrough();
22
+ const feedSchema = z.object({
23
+ _id: z.string().min(1, 'Feed _id is required'),
24
+ title: z.string().min(1, 'Feed title is required').max(60, 'Feed title max 60 chars'),
25
+ description: z.string().optional(),
26
+ imageSrc: z.string(),
27
+ mode: z.string(),
28
+ scrollDirection: z.string(),
29
+ engagement: z.boolean().optional(),
30
+ }).passthrough();
31
+ const sizingEnum = z.enum(['automatic', 'wide', 'portrait', 'square', 'small_portrait']);
32
+ const socialSourceEnum = z.enum(['x', 'linkedin', 'instagram', 'facebook', 'threads', 'bluesky']);
33
+ const buttonActionEnum = z.enum(['url', 'fork', 'feed', 'user']);
34
+ const quizOptionSchema = z.object({
35
+ label: z.string().min(1),
36
+ correct: z.boolean(),
37
+ }).passthrough();
38
+ const contentBlockSchema = z.discriminatedUnion('type', [
39
+ z.object({ type: z.literal('CONTENT_IMAGE'), imageSrc: z.string().min(1), sizing: sizingEnum }).passthrough(),
40
+ z.object({ type: z.literal('CONTENT_TITLE'), title: z.string().min(1) }).passthrough(),
41
+ z.object({ type: z.literal('CONTENT_TEXT'), text: z.string().min(1) }).passthrough(),
42
+ z.object({ type: z.literal('CONTENT_CODE'), code: z.string().min(1), language: z.string().optional() }).passthrough(),
43
+ z.object({ type: z.literal('CONTENT_SOCIAL'), name: z.string().min(1), avatarSrc: z.string(), source: socialSourceEnum }).passthrough(),
44
+ z.object({ type: z.literal('CONTENT_SUBTEXT'), text: z.string().min(1) }).passthrough(),
45
+ z.object({ type: z.literal('CONTENT_QUIZ'), question: z.string().min(1), options: z.array(quizOptionSchema).min(2), explanation: z.string().optional() }).passthrough(),
46
+ z.object({ type: z.literal('CONTENT_BUTTON'), label: z.string().min(1), action: buttonActionEnum, target: z.string().min(1) }).passthrough(),
47
+ ]);
48
+ const variantSchema = z.discriminatedUnion('type', [
49
+ z.object({ type: z.literal('FULL_IMAGE'), imageSrc: z.string().min(1, 'FULL_IMAGE requires imageSrc'), title: z.string().optional(), subtitle: z.string().optional() }).passthrough(),
50
+ z.object({ type: z.literal('FULL_VIDEO'), videoSrc: z.string().min(1), title: z.string().optional() }).passthrough(),
51
+ z.object({ type: z.literal('CONTENT'), blocks: z.array(contentBlockSchema).min(1, 'CONTENT variant needs at least one block'), backgroundSrc: z.string().optional() }).passthrough(),
52
+ ]);
53
+ const cardSchema = z.object({
54
+ _id: z.string().min(1, 'Card _id is required'),
55
+ feedId: z.string().min(1, 'Card feedId is required'),
56
+ order: z.number().int().min(0),
57
+ variants: z.array(variantSchema).min(2, 'Card needs at least 2 variants (cover + detail)'),
58
+ }).passthrough();
59
+ const manifestSchema = z.object({
60
+ forks: z.array(forkSchema).min(1, 'Manifest must have at least one fork'),
61
+ feeds: z.array(feedSchema).min(1, 'Manifest must have at least one feed'),
62
+ cards: z.array(cardSchema).min(1, 'Manifest must have at least one card'),
63
+ });
64
+ /** Cross-reference checks that Zod can't express. Returns error string or null. */
65
+ function crossValidate(manifest) {
66
+ const feedIds = new Set(manifest.feeds.map((f) => f._id));
67
+ const errors = [];
68
+ for (const fork of manifest.forks) {
69
+ for (const fid of fork.feedIds) {
70
+ if (!feedIds.has(fid))
71
+ errors.push(`Fork "${fork._id}" references feed "${fid}" which is not in feeds array`);
72
+ }
73
+ }
74
+ for (const card of manifest.cards) {
75
+ if (!feedIds.has(card.feedId))
76
+ errors.push(`Card "${card._id}" references feed "${card.feedId}" which is not in feeds array`);
77
+ for (let vi = 0; vi < card.variants.length; vi++) {
78
+ const v = card.variants[vi];
79
+ if (v.type !== 'CONTENT')
80
+ continue;
81
+ for (const block of v.blocks) {
82
+ if (block.type === 'CONTENT_QUIZ') {
83
+ const hasCorrect = block.options.some((o) => o.correct === true);
84
+ if (!hasCorrect)
85
+ errors.push(`Card "${card._id}" variants[${vi}]: CONTENT_QUIZ must have at least one correct option`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return errors.length > 0 ? errors.join('\n') : null;
91
+ }
12
92
  // ── Helpers ────────────────────────────────────────────────────────────
13
93
  function authHeaders() {
14
94
  return {
@@ -67,6 +147,24 @@ server.tool('forkfeed_push', 'Push a generated manifest (forks, feeds, cards) to
67
147
  isError: true,
68
148
  };
69
149
  }
150
+ // Validate manifest structure before pushing
151
+ const parsed = manifestSchema.safeParse(manifest);
152
+ if (!parsed.success) {
153
+ const issues = parsed.error.issues
154
+ .map((i) => ` ${i.path.join('.')}: ${i.message}`)
155
+ .join('\n');
156
+ return {
157
+ content: [{ type: 'text', text: `Manifest validation failed:\n${issues}\n\nFix the issues above and try again.` }],
158
+ isError: true,
159
+ };
160
+ }
161
+ const crossErr = crossValidate(parsed.data);
162
+ if (crossErr) {
163
+ return {
164
+ content: [{ type: 'text', text: `Manifest cross-reference errors:\n${crossErr}\n\nFix the issues above and try again.` }],
165
+ isError: true,
166
+ };
167
+ }
70
168
  // Save manifest locally before uploading (best-effort, doesn't block push)
71
169
  const forkId = manifest.forks?.[0]?._id;
72
170
  const filename = `${forkId || `manifest-${Date.now()}`}.json`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forkfeed-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "MCP server for pushing GitHub commits to forkfeed",
5
5
  "type": "module",
6
6
  "bin": {