vyra-mcp 0.2.0 → 0.4.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/README.md CHANGED
@@ -6,24 +6,37 @@ performance-based creator campaigns in plain English.
6
6
  ## Quick start
7
7
 
8
8
  1. Create an API key in Vyra: **Settings → Developers → Create key**.
9
- 2. Connect it:
9
+ 2. Connect it — one command auto-configures every MCP client you have installed
10
+ (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code):
10
11
 
11
12
  ```bash
12
13
  npx vyra-mcp connect vyra_sk_your_key_here
13
14
  ```
14
15
 
15
- 3. Restart Claude Desktop, then ask: _"List my Vyra campaigns"_ or
16
+ 3. Restart the client, then ask: _"List my Vyra campaigns"_ or
16
17
  _"Create a TikTok campaign with a $5 fee and a $500 budget, then activate it."_
17
18
 
19
+ ### Options (rarely needed)
20
+
21
+ ```bash
22
+ npx vyra-mcp connect <key> --client cursor # only one client
23
+ npx vyra-mcp connect <key> --print # just print a snippet (e.g. ChatGPT Desktop)
24
+ ```
25
+
18
26
  ## Tools
19
27
 
28
+ On connect, the server tells your agent how Vyra works (performance-based creator
29
+ marketing, **not** paid ads) so it won't ask irrelevant ad-platform questions.
30
+
20
31
  | Tool | What it does |
21
32
  |------|--------------|
33
+ | `get_started` | Call first — how Vyra works + live wallet/campaigns + required fields |
22
34
  | `get_wallet_balance` | Available prepaid balance |
23
35
  | `list_campaigns` | All your campaigns |
24
36
  | `get_campaign` | One campaign by id |
25
37
  | `list_campaign_slots` | Creators who joined, with status + views |
26
38
  | `get_submission` | One submission's metrics, cost, watch-window |
39
+ | `estimate_campaign` | Preview how many creators a budget funds (no spend) |
27
40
  | `create_campaign` | Create a draft campaign (optionally with assets) |
28
41
  | `activate_campaign` | Fund-check, reserve budget, go live |
29
42
  | `pause_campaign` | Stop new creators from joining |
package/dist/cli.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // Entry point for the `vyra-mcp` binary.
3
3
  //
4
- // vyra-mcp connect <vyra_sk_…> [--base-url <url>] wire into Claude Desktop
4
+ // vyra-mcp connect <vyra_sk_…> [--client <id|all>] [--base-url <url>] [--print]
5
5
  // vyra-mcp serve run the MCP server (stdio)
6
6
  // vyra-mcp same as `serve`
7
7
  //
8
+ // connect targets Claude Desktop by default. --client picks another client
9
+ // (claude-code, cursor, windsurf, vscode), `all` configures every detected one,
10
+ // and --print shows a snippet for manual setup (e.g. ChatGPT Desktop).
11
+ //
8
12
  // `serve` is the default so MCP clients can spawn the bare binary.
9
13
  import { connect } from "./connect.js";
10
14
  import { serve } from "./server.js";
@@ -18,11 +22,15 @@ async function main() {
18
22
  case "connect": {
19
23
  const key = rest.find((a) => !a.startsWith("--"));
20
24
  if (!key) {
21
- console.error("Usage: npx vyra-mcp connect <your-api-key>");
25
+ console.error("Usage: npx vyra-mcp connect <your-api-key> [--client <id|all>] [--print]");
22
26
  process.exitCode = 1;
23
27
  return;
24
28
  }
25
- connect(key, getFlag(rest, "--base-url"));
29
+ connect(key, {
30
+ client: getFlag(rest, "--client"),
31
+ baseUrl: getFlag(rest, "--base-url"),
32
+ print: rest.includes("--print") || rest.includes("--manual"),
33
+ });
26
34
  return;
27
35
  }
28
36
  case undefined:
@@ -0,0 +1,122 @@
1
+ // Registry of MCP clients we can auto-configure, plus the logic to merge our
2
+ // "vyra" server into each one's config file.
3
+ //
4
+ // Most clients share the `{ mcpServers: { vyra: { command, args, env } } }`
5
+ // shape. VS Code is the odd one out (`servers`, with an explicit `type`).
6
+ // ChatGPT Desktop has no stable local-stdio config file — users add it through
7
+ // its connector UI — so it's covered by the printed manual snippet instead.
8
+ import { homedir, platform } from "node:os";
9
+ import { join, dirname } from "node:path";
10
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
11
+ // Claude Desktop config path, per OS.
12
+ function claudeDesktopPath() {
13
+ const home = homedir();
14
+ switch (platform()) {
15
+ case "darwin":
16
+ return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
17
+ case "win32":
18
+ return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
19
+ default:
20
+ return join(home, ".config", "Claude", "claude_desktop_config.json");
21
+ }
22
+ }
23
+ export const CLIENTS = [
24
+ {
25
+ id: "claude-desktop",
26
+ label: "Claude Desktop",
27
+ format: "mcpServers",
28
+ resolvePath: claudeDesktopPath,
29
+ },
30
+ {
31
+ id: "claude-code",
32
+ label: "Claude Code (CLI)",
33
+ format: "mcpServers",
34
+ resolvePath: () => join(homedir(), ".claude.json"), // user scope
35
+ },
36
+ {
37
+ id: "cursor",
38
+ label: "Cursor",
39
+ format: "mcpServers",
40
+ resolvePath: () => join(homedir(), ".cursor", "mcp.json"),
41
+ },
42
+ {
43
+ id: "windsurf",
44
+ label: "Windsurf",
45
+ format: "mcpServers",
46
+ resolvePath: () => join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
47
+ },
48
+ {
49
+ id: "vscode",
50
+ label: "VS Code (Copilot)",
51
+ format: "vscode",
52
+ resolvePath: () => join(process.cwd(), ".vscode", "mcp.json"),
53
+ },
54
+ ];
55
+ export function getClient(id) {
56
+ return CLIENTS.find((c) => c.id === id);
57
+ }
58
+ // The server entry we inject. `npx -y` runs it with no global install. VS Code
59
+ // needs an explicit "stdio" type field; everyone else doesn't.
60
+ export function serverEntry(apiKey, baseUrl, vscode = false) {
61
+ const env = { VYRA_API_KEY: apiKey };
62
+ if (baseUrl)
63
+ env.VYRA_BASE_URL = baseUrl;
64
+ const base = { command: "npx", args: ["-y", "vyra-mcp", "serve"], env };
65
+ return vscode ? { type: "stdio", ...base } : base;
66
+ }
67
+ // Merge our server into a client's config, preserving everything else. Returns
68
+ // a result rather than throwing so the caller can report per-client outcomes.
69
+ export function writeClientConfig(client, apiKey, baseUrl) {
70
+ const path = client.resolvePath();
71
+ // Read existing config (treat missing/empty as {}). Never clobber a file we
72
+ // can't parse — report it and let the caller fall back to the manual snippet.
73
+ let config = {};
74
+ if (existsSync(path)) {
75
+ try {
76
+ const raw = readFileSync(path, "utf8");
77
+ if (raw.trim()) {
78
+ const parsed = JSON.parse(raw);
79
+ if (parsed && typeof parsed === "object") {
80
+ config = parsed;
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ return { ok: false, path, error: "existing config isn't valid JSON" };
86
+ }
87
+ }
88
+ const key = client.format === "vscode" ? "servers" : "mcpServers";
89
+ const entry = serverEntry(apiKey, baseUrl, client.format === "vscode");
90
+ const existing = config[key] && typeof config[key] === "object"
91
+ ? config[key]
92
+ : {};
93
+ // Immutable merge: keep every other server, add/replace ours.
94
+ const next = { ...config, [key]: { ...existing, vyra: entry } };
95
+ try {
96
+ mkdirSync(dirname(path), { recursive: true });
97
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf8");
98
+ return { ok: true, path };
99
+ }
100
+ catch (err) {
101
+ const reason = err instanceof Error ? err.message : "write failed";
102
+ return { ok: false, path, error: reason };
103
+ }
104
+ }
105
+ // Is this client plausibly installed? Used by auto-detect so we don't scatter
106
+ // config files for apps the user doesn't have. A client counts as present if
107
+ // its config file exists, or its parent dir exists AND that parent is
108
+ // client-specific (not the home dir or cwd — those exist everywhere, e.g.
109
+ // Claude Code's ~/.claude.json or VS Code's ./.vscode).
110
+ export function isClientDetected(client) {
111
+ const path = client.resolvePath();
112
+ if (existsSync(path))
113
+ return true;
114
+ const dir = dirname(path);
115
+ if (dir === homedir() || dir === process.cwd())
116
+ return false;
117
+ return existsSync(dir);
118
+ }
119
+ // Copy-paste config for any client we don't write automatically (e.g. ChatGPT).
120
+ export function manualSnippet(apiKey, baseUrl) {
121
+ return JSON.stringify({ mcpServers: { vyra: serverEntry(apiKey, baseUrl) } }, null, 2);
122
+ }
package/dist/connect.js CHANGED
@@ -1,98 +1,68 @@
1
- // `vyra-mcp connect <key>` one-command setup. Adds (or updates) a "vyra" entry
2
- // in the Claude Desktop config so Claude launches this server with the brand's
3
- // API key. Other MCP clients can copy the same snippet manually.
4
- import { homedir, platform } from "node:os";
5
- import { join } from "node:path";
6
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
7
- // Where Claude Desktop keeps its config, per OS.
8
- function claudeConfigPath() {
9
- const home = homedir();
10
- switch (platform()) {
11
- case "darwin":
12
- return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
13
- case "win32":
14
- return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
15
- default:
16
- // Linux / others
17
- return join(home, ".config", "Claude", "claude_desktop_config.json");
1
+ // `vyra-mcp connect <key> [--client <id|all>] [--base-url <url>] [--print]`
2
+ //
3
+ // Wires the Vyra MCP server into one or more MCP clients. Default target is
4
+ // Claude Desktop. Use --client to pick another (claude-code, cursor, windsurf,
5
+ // vscode), --client all to configure every detected client, or --print to just
6
+ // show the config snippet for manual setup (e.g. ChatGPT Desktop).
7
+ import { CLIENTS, getClient, writeClientConfig, isClientDetected, manualSnippet, } from "./clients.js";
8
+ // Decide which clients to write to. With no --client flag we auto-detect and
9
+ // configure EVERY installed client, so the common case is just one command:
10
+ // npx vyra-mcp connect <key>
11
+ function resolveTargets(client) {
12
+ if (!client || client === "all") {
13
+ // Only touch clients that look installed, so we don't scatter stray files.
14
+ return CLIENTS.filter(isClientDetected);
18
15
  }
19
- }
20
- // Read the existing config if present; return {} on missing/corrupt so we never
21
- // wipe a user's setup because of a parse error.
22
- function readConfig(path) {
23
- if (!existsSync(path))
24
- return {};
25
- try {
26
- const raw = readFileSync(path, "utf8");
27
- if (!raw.trim())
28
- return {};
29
- const parsed = JSON.parse(raw);
30
- return typeof parsed === "object" && parsed !== null
31
- ? parsed
32
- : {};
33
- }
34
- catch {
35
- console.warn("⚠️ Existing Claude config wasn't valid JSON — leaving it untouched and printing manual steps instead.");
36
- throw new Error("UNREADABLE_CONFIG");
16
+ const def = getClient(client);
17
+ if (!def) {
18
+ const ids = CLIENTS.map((c) => c.id).join(", ");
19
+ return `Unknown client "${client}". Supported: ${ids}, all.`;
37
20
  }
21
+ return [def];
38
22
  }
39
- // The server entry we inject. `npx -y` lets Claude run it with no global install.
40
- function vyraEntry(apiKey, baseUrl) {
41
- const env = { VYRA_API_KEY: apiKey };
42
- if (baseUrl)
43
- env.VYRA_BASE_URL = baseUrl;
44
- return {
45
- command: "npx",
46
- args: ["-y", "vyra-mcp", "serve"],
47
- env,
48
- };
49
- }
50
- // Print a copy-paste snippet for clients we can't auto-configure.
51
- function printManual(apiKey, baseUrl) {
52
- const snippet = {
53
- mcpServers: { vyra: vyraEntry(apiKey, baseUrl) },
54
- };
55
- console.log("\nAdd this to your MCP client config manually:\n");
56
- console.log(JSON.stringify(snippet, null, 2));
57
- console.log("");
58
- }
59
- export function connect(apiKey, baseUrl) {
23
+ export function connect(apiKey, opts = {}) {
60
24
  const trimmed = apiKey.trim();
61
25
  if (!trimmed || !trimmed.startsWith("vyra_sk_")) {
62
26
  console.error("That doesn't look like a Vyra key. Create one in Settings → Developers (it starts with vyra_sk_).");
63
27
  process.exitCode = 1;
64
28
  return;
65
29
  }
66
- const path = claudeConfigPath();
67
- let config;
68
- try {
69
- config = readConfig(path);
70
- }
71
- catch {
72
- // Corrupt existing config — don't touch it, just show manual steps.
73
- printManual(trimmed, baseUrl);
30
+ // Manual mode — just print the snippet for hand-configuring any client.
31
+ if (opts.print) {
32
+ console.log("\nAdd this to your MCP client config:\n");
33
+ console.log(manualSnippet(trimmed, opts.baseUrl));
34
+ console.log("");
74
35
  return;
75
36
  }
76
- // Immutable merge: keep every existing server, add/replace ours.
77
- const next = {
78
- ...config,
79
- mcpServers: {
80
- ...(config.mcpServers ?? {}),
81
- vyra: vyraEntry(trimmed, baseUrl),
82
- },
83
- };
84
- try {
85
- mkdirSync(join(path, ".."), { recursive: true });
86
- writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf8");
87
- }
88
- catch (err) {
89
- const reason = err instanceof Error ? err.message : "write failed";
90
- console.error(`Couldn't write ${path}: ${reason}`);
91
- printManual(trimmed, baseUrl);
37
+ const targets = resolveTargets(opts.client);
38
+ if (typeof targets === "string") {
39
+ console.error(targets);
92
40
  process.exitCode = 1;
93
41
  return;
94
42
  }
95
- console.log("✓ Vyra connected to Claude Desktop.");
96
- console.log(` Config: ${path}`);
97
- console.log(" Restart Claude Desktop, then ask it to list your campaigns.");
43
+ if (targets.length === 0) {
44
+ // Nothing auto-detected — just hand them the snippet to paste, rather than
45
+ // making them run another command.
46
+ console.log("No MCP client auto-detected. Paste this into your client's MCP config:\n");
47
+ console.log(manualSnippet(trimmed, opts.baseUrl));
48
+ console.log("");
49
+ return;
50
+ }
51
+ let anyOk = false;
52
+ for (const client of targets) {
53
+ const res = writeClientConfig(client, trimmed, opts.baseUrl);
54
+ if (res.ok) {
55
+ anyOk = true;
56
+ console.log(`✓ ${client.label} — ${res.path}`);
57
+ }
58
+ else {
59
+ console.log(`• ${client.label} — skipped (${res.error})`);
60
+ }
61
+ }
62
+ if (anyOk) {
63
+ console.log("\nRestart the client, then ask it to list your Vyra campaigns.");
64
+ }
65
+ else {
66
+ console.log("\nNothing was written. Use --print for a manual snippet.");
67
+ }
98
68
  }
package/dist/server.js CHANGED
@@ -7,6 +7,25 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { z } from "zod";
9
9
  import { apiRequest } from "./api.js";
10
+ // Injected into the agent's context the moment it connects. This is what stops
11
+ // agents from asking generic ad-platform questions (daily budget, destination
12
+ // URL, ad placements) — Vyra is not paid ads.
13
+ const INSTRUCTIONS = `Vyra is a PERFORMANCE-BASED CREATOR MARKETING platform — NOT a paid-ads tool (not Meta/Google/TikTok Ads).
14
+
15
+ How it works: a brand funds prepaid credits, then creates a campaign that pays small creators to post short organic videos to their OWN audiences. Each creator earns a FIXED FEE when their video is approved, plus an optional PERFORMANCE BONUS based on views over a watch window. The brand buys "slots" — one creator per slot.
16
+
17
+ So DO NOT ask about: daily ad budget, destination/landing URL as an ad target, ad placements, bid strategy, conversion tracking pixels, or audiences-to-target-with-ads. Those don't apply.
18
+
19
+ Recommended workflow when a user wants to run a campaign:
20
+ 1. Call get_started first — it returns the brand's live wallet balance, existing campaigns, and the exact fields a campaign needs. Ground yourself in that before asking anything.
21
+ 2. To create a campaign you only NEED: a name, a fixed_fee_cents (guaranteed pay per creator on approval), and a budget_cents (total — budget ÷ per-creator cost = number of creator slots). Everything else has sensible defaults.
22
+ 3. Helpful extras: key_message (the brief — what creators should highlight), a bonus (bonus_type "milestone" or "cpm" + bonus_cap_cents), targeting (countries, min/max_followers), content rules (watch_window_days, min/max_video_seconds, submission_deadline_hours), hashtags, promo_link, and assets (logo/demo video/screenshots attached BY URL).
23
+ 4. create_campaign makes a DRAFT — it is not live and spends nothing yet.
24
+ 5. To go live, call activate_campaign. This RESERVES the budget from the wallet, so the wallet must be funded first (check get_wallet_balance). If funds are too low, tell the user to add credits in the Vyra dashboard.
25
+
26
+ NEVER INVENT NUMBERS. Do not guess or calculate slot counts, costs, or balances yourself. Always read them from tool results: get_wallet_balance for funds, estimate_campaign to preview how many creators a budget funds, and the value create_campaign returns for the real slot count. If you don't have a number from a tool, call the tool — don't make one up.
27
+
28
+ IMPORTANT: All money fields are integer CENTS (e.g. $5.00 = 500). Platform fees/margins are internal — never ask about or expose them. Confirm the draft with the user before activating (activation spends real credits).`;
10
29
  // Wrap any tool body so a thrown error becomes a clean MCP error result instead
11
30
  // of crashing the transport. Success returns the value as pretty JSON text.
12
31
  async function run(fn) {
@@ -20,7 +39,49 @@ async function run(fn) {
20
39
  }
21
40
  }
22
41
  export function buildServer() {
23
- const server = new McpServer({ name: "vyra", version: "0.2.0" });
42
+ const server = new McpServer({ name: "vyra", version: "0.3.0" }, { instructions: INSTRUCTIONS });
43
+ // --- Orientation --------------------------------------------------------
44
+ // One call an agent makes first to ground itself: live state + how Vyra works
45
+ // + exactly what a campaign needs. Stops it from asking irrelevant questions.
46
+ server.registerTool("get_started", {
47
+ title: "Get started / account overview",
48
+ description: "Call this FIRST. Returns how Vyra works, the brand's live wallet balance and existing campaigns, and the exact fields needed to create a campaign. Use it to avoid asking the user for anything Vyra doesn't need.",
49
+ inputSchema: {},
50
+ }, () => run(async () => {
51
+ // Pull live state; tolerate either call failing so orientation still works.
52
+ const [wallet, campaigns] = await Promise.all([
53
+ apiRequest("GET", "/wallet").catch(() => null),
54
+ apiRequest("GET", "/campaigns").catch(() => null),
55
+ ]);
56
+ return {
57
+ platform: "Vyra — performance-based creator marketing. Brands pay small creators a fixed fee + performance bonus to post short organic videos. NOT paid ads.",
58
+ wallet,
59
+ campaigns,
60
+ to_create_a_campaign: {
61
+ required: {
62
+ name: "Campaign name",
63
+ fixed_fee_cents: "Guaranteed pay per creator on approval (cents)",
64
+ budget_cents: "Total budget (cents). budget ÷ per-creator cost = creator slots",
65
+ },
66
+ optional: {
67
+ key_message: "The brief — what creators should highlight",
68
+ bonus: "bonus_type 'milestone'|'cpm' + bonus_cap_cents (and bonus_cpm_cents for cpm)",
69
+ targeting: "countries[], min_followers, max_followers",
70
+ content_rules: "watch_window_days, min_video_seconds, max_video_seconds, submission_deadline_hours",
71
+ hashtags: "hashtags[]",
72
+ promo_link: "Link or discount code",
73
+ assets: "Attach logo/demo video/screenshots by URL (attach_campaign_asset)",
74
+ platform: "tiktok (default) | instagram | youtube",
75
+ },
76
+ notes: [
77
+ "All money is integer cents ($5 = 500).",
78
+ "create_campaign makes a DRAFT (spends nothing).",
79
+ "activate_campaign reserves budget from the wallet — fund it first.",
80
+ "Do NOT ask about ad budgets, placements, or destination URLs — Vyra is not paid ads.",
81
+ ],
82
+ },
83
+ };
84
+ }));
24
85
  // --- Wallet -------------------------------------------------------------
25
86
  server.registerTool("get_wallet_balance", {
26
87
  title: "Get wallet balance",
@@ -56,6 +117,22 @@ export function buildServer() {
56
117
  },
57
118
  }, ({ submission_id }) => run(() => apiRequest("GET", `/submissions/${submission_id}`)));
58
119
  // --- Campaigns: write ---------------------------------------------------
120
+ server.registerTool("estimate_campaign", {
121
+ title: "Estimate creator slots",
122
+ description: "Preview how many creator slots a budget funds, WITHOUT creating anything. Call this to show the user real numbers before creating a campaign — never compute slot counts yourself.",
123
+ inputSchema: {
124
+ fixed_fee_cents: z
125
+ .number()
126
+ .int()
127
+ .describe("Guaranteed fee per creator, in cents"),
128
+ budget_cents: z.number().int().describe("Total budget, in cents"),
129
+ bonus_cap_cents: z
130
+ .number()
131
+ .int()
132
+ .optional()
133
+ .describe("Max performance bonus per creator, in cents"),
134
+ },
135
+ }, (args) => run(() => apiRequest("POST", "/campaigns/estimate", args)));
59
136
  server.registerTool("create_campaign", {
60
137
  title: "Create campaign (draft)",
61
138
  description: "Create a new draft campaign. It is NOT live yet — call activate_campaign after funding. All money fields are integer cents.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vyra-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",