vyra-mcp 0.4.0 → 0.5.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/api.js CHANGED
@@ -55,6 +55,12 @@ export async function apiRequest(method, path, body) {
55
55
  }
56
56
  }
57
57
  if (!res.ok) {
58
+ // A 401 here means a key was sent but rejected — point the user at the fix
59
+ // instead of surfacing the bare "Invalid or missing API key" envelope.
60
+ if (res.status === 401) {
61
+ throw new Error("Vyra rejected the API key (HTTP 401). Generate a fresh key in " +
62
+ "Settings → Developers and re-run `npx vyra-mcp connect <your-key>`.");
63
+ }
58
64
  const message = json?.error ??
59
65
  `HTTP ${res.status} from Vyra`;
60
66
  throw new Error(message);
package/dist/server.js CHANGED
@@ -38,8 +38,92 @@ async function run(fn) {
38
38
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
39
39
  }
40
40
  }
41
+ // Campaign draft fields, shared by create_campaign and update_campaign so the
42
+ // two never drift. Money fields are integer cents.
43
+ const CAMPAIGN_FIELDS = {
44
+ name: z.string().describe("Campaign name"),
45
+ fixed_fee_cents: z
46
+ .number()
47
+ .int()
48
+ .describe("Guaranteed fee paid to each creator on approval, in cents"),
49
+ budget_cents: z.number().int().describe("Total campaign budget in cents"),
50
+ objective: z.string().optional().describe("Short objective/goal"),
51
+ platform: z
52
+ .enum(["tiktok", "instagram", "youtube"])
53
+ .optional()
54
+ .describe("Target platform (default tiktok)"),
55
+ countries: z
56
+ .array(z.string())
57
+ .optional()
58
+ .describe("Allowed 2-letter country codes"),
59
+ bonus_cap_cents: z
60
+ .number()
61
+ .int()
62
+ .optional()
63
+ .describe("Max performance bonus per creator, in cents"),
64
+ bonus_type: z.enum(["milestone", "cpm", "none"]).optional(),
65
+ bonus_cpm_cents: z
66
+ .number()
67
+ .int()
68
+ .optional()
69
+ .describe("Bonus per 1000 views, in cents (when bonus_type is cpm)"),
70
+ languages: z
71
+ .array(z.string())
72
+ .optional()
73
+ .describe("Allowed creator languages"),
74
+ min_followers: z.number().int().optional(),
75
+ max_followers: z.number().int().optional(),
76
+ min_quality_score: z
77
+ .number()
78
+ .int()
79
+ .min(0)
80
+ .max(100)
81
+ .optional()
82
+ .describe("Minimum AI quality score (0–100) a submission must hit"),
83
+ require_connected_account: z
84
+ .boolean()
85
+ .optional()
86
+ .describe("Require creators to have a connected social account"),
87
+ watch_window_days: z
88
+ .number()
89
+ .int()
90
+ .optional()
91
+ .describe("Days to track views before the bonus settles"),
92
+ submission_deadline_hours: z
93
+ .number()
94
+ .int()
95
+ .optional()
96
+ .describe("Hours a creator has to post after joining"),
97
+ min_video_seconds: z
98
+ .number()
99
+ .int()
100
+ .optional()
101
+ .describe("Minimum video length, in seconds"),
102
+ max_video_seconds: z
103
+ .number()
104
+ .int()
105
+ .optional()
106
+ .describe("Maximum video length, in seconds"),
107
+ key_message: z
108
+ .string()
109
+ .optional()
110
+ .describe("The brief / what creators should communicate"),
111
+ hashtags: z.array(z.string()).optional(),
112
+ promo_link: z
113
+ .string()
114
+ .optional()
115
+ .describe("A link OR a plain discount code (e.g. SAVE20)"),
116
+ assets: z
117
+ .array(z.object({
118
+ kind: z.enum(["logo", "video", "screenshot"]),
119
+ url: z.string().describe("Hosted URL of the asset"),
120
+ file_name: z.string().optional(),
121
+ }))
122
+ .optional()
123
+ .describe("Brand assets to attach, by URL (logo/demo video/screenshots)"),
124
+ };
41
125
  export function buildServer() {
42
- const server = new McpServer({ name: "vyra", version: "0.3.0" }, { instructions: INSTRUCTIONS });
126
+ const server = new McpServer({ name: "vyra", version: "0.5.0" }, { instructions: INSTRUCTIONS });
43
127
  // --- Orientation --------------------------------------------------------
44
128
  // One call an agent makes first to ground itself: live state + how Vyra works
45
129
  // + exactly what a campaign needs. Stops it from asking irrelevant questions.
@@ -88,6 +172,17 @@ export function buildServer() {
88
172
  description: "Get the brand's available prepaid balance (in cents and USD). Use before activating a campaign to confirm funds.",
89
173
  inputSchema: {},
90
174
  }, () => run(() => apiRequest("GET", "/wallet")));
175
+ server.registerTool("create_topup_link", {
176
+ title: "Add credits (Stripe checkout link)",
177
+ description: "Start a Stripe Checkout to add credits to the wallet and return the hosted URL. Card payment happens in the browser (a card can't be charged headlessly), so give the user the link to open once — the balance updates automatically after payment. 1 credit = $1. Use this when activate_campaign reports insufficient funds.",
178
+ inputSchema: {
179
+ credits: z
180
+ .number()
181
+ .int()
182
+ .min(1)
183
+ .describe("Credits to add (1 credit = $1)"),
184
+ },
185
+ }, ({ credits }) => run(() => apiRequest("POST", "/wallet/topup", { credits })));
91
186
  // --- Campaigns: read ----------------------------------------------------
92
187
  server.registerTool("list_campaigns", {
93
188
  title: "List campaigns",
@@ -136,58 +231,13 @@ export function buildServer() {
136
231
  server.registerTool("create_campaign", {
137
232
  title: "Create campaign (draft)",
138
233
  description: "Create a new draft campaign. It is NOT live yet — call activate_campaign after funding. All money fields are integer cents.",
139
- inputSchema: {
140
- name: z.string().describe("Campaign name"),
141
- fixed_fee_cents: z
142
- .number()
143
- .int()
144
- .describe("Guaranteed fee paid to each creator on approval, in cents"),
145
- budget_cents: z
146
- .number()
147
- .int()
148
- .describe("Total campaign budget in cents"),
149
- objective: z.string().optional().describe("Short objective/goal"),
150
- platform: z
151
- .enum(["tiktok", "instagram", "youtube"])
152
- .optional()
153
- .describe("Target platform (default tiktok)"),
154
- countries: z
155
- .array(z.string())
156
- .optional()
157
- .describe("Allowed 2-letter country codes"),
158
- bonus_cap_cents: z
159
- .number()
160
- .int()
161
- .optional()
162
- .describe("Max performance bonus per creator, in cents"),
163
- bonus_type: z.enum(["milestone", "cpm", "none"]).optional(),
164
- bonus_cpm_cents: z
165
- .number()
166
- .int()
167
- .optional()
168
- .describe("Bonus per 1000 views, in cents (when bonus_type is cpm)"),
169
- min_followers: z.number().int().optional(),
170
- watch_window_days: z
171
- .number()
172
- .int()
173
- .optional()
174
- .describe("Days to track views before the bonus settles"),
175
- key_message: z
176
- .string()
177
- .optional()
178
- .describe("The brief / what creators should communicate"),
179
- hashtags: z.array(z.string()).optional(),
180
- promo_link: z.string().optional(),
181
- assets: z
182
- .array(z.object({
183
- kind: z.enum(["logo", "video", "screenshot"]),
184
- url: z.string().describe("Hosted URL of the asset"),
185
- file_name: z.string().optional(),
186
- }))
187
- .optional()
188
- .describe("Brand assets to attach, by URL (logo/demo video/screenshots)"),
189
- },
234
+ inputSchema: CAMPAIGN_FIELDS,
190
235
  }, (args) => run(() => apiRequest("POST", "/campaigns", args)));
236
+ server.registerTool("update_campaign", {
237
+ title: "Update campaign (draft only)",
238
+ description: "Edit a DRAFT campaign's fields (only drafts can be edited — a live campaign can't be changed). Pass the full set of fields the draft should have; money fields are integer cents.",
239
+ inputSchema: { campaign_id: z.string().describe("The campaign id"), ...CAMPAIGN_FIELDS },
240
+ }, ({ campaign_id, ...fields }) => run(() => apiRequest("PATCH", `/campaigns/${campaign_id}`, fields)));
191
241
  server.registerTool("list_campaign_assets", {
192
242
  title: "List campaign assets",
193
243
  description: "List the brand assets (logo, demo video, screenshots) attached to a campaign.",
@@ -223,6 +273,20 @@ export function buildServer() {
223
273
  campaign_id: z.string().describe("The campaign id to pause"),
224
274
  },
225
275
  }, ({ campaign_id }) => run(() => apiRequest("POST", `/campaigns/${campaign_id}/pause`)));
276
+ server.registerTool("cancel_campaign", {
277
+ title: "Cancel campaign",
278
+ description: "Close a campaign. Releases the reserved budget for unfilled slots back to the wallet and refunds join fees to creators who joined but won't earn. Cannot be undone.",
279
+ inputSchema: {
280
+ campaign_id: z.string().describe("The campaign id to cancel"),
281
+ },
282
+ }, ({ campaign_id }) => run(() => apiRequest("POST", `/campaigns/${campaign_id}/cancel`)));
283
+ server.registerTool("get_campaign_report", {
284
+ title: "Get campaign report",
285
+ description: "Performance report for a campaign: creators joined, videos live, approval rate, verified views, engagement, the views trend, and a per-submission breakdown.",
286
+ inputSchema: {
287
+ campaign_id: z.string().describe("The campaign id"),
288
+ },
289
+ }, ({ campaign_id }) => run(() => apiRequest("GET", `/campaigns/${campaign_id}/report`)));
226
290
  return server;
227
291
  }
228
292
  // Start the server on stdio (how MCP clients spawn it as a child process).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vyra-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Drive Vyra from Claude, ChatGPT, or any MCP client — manage performance-based creator campaigns in plain English.",
5
5
  "license": "MIT",
6
6
  "type": "module",