vyra-mcp 0.1.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 ADDED
@@ -0,0 +1,57 @@
1
+ # vyra-mcp
2
+
3
+ Drive [Vyra](https://creator-networks-sigma.vercel.app) from Claude, ChatGPT, or any MCP client — manage
4
+ performance-based creator campaigns in plain English.
5
+
6
+ ## Quick start
7
+
8
+ 1. Create an API key in Vyra: **Settings → Developers → Create key**.
9
+ 2. Connect it:
10
+
11
+ ```bash
12
+ npx vyra-mcp connect vyra_sk_your_key_here
13
+ ```
14
+
15
+ 3. Restart Claude Desktop, then ask: _"List my Vyra campaigns"_ or
16
+ _"Create a TikTok campaign with a $5 fee and a $500 budget, then activate it."_
17
+
18
+ ## Tools
19
+
20
+ | Tool | What it does |
21
+ |------|--------------|
22
+ | `get_wallet_balance` | Available prepaid balance |
23
+ | `list_campaigns` | All your campaigns |
24
+ | `get_campaign` | One campaign by id |
25
+ | `list_campaign_slots` | Creators who joined, with status + views |
26
+ | `get_submission` | One submission's metrics, cost, watch-window |
27
+ | `create_campaign` | Create a draft campaign |
28
+ | `activate_campaign` | Fund-check, reserve budget, go live |
29
+ | `pause_campaign` | Stop new creators from joining |
30
+
31
+ ## Manual config
32
+
33
+ `connect` writes a `vyra` entry into your Claude Desktop config. To wire another
34
+ client by hand:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "vyra": {
40
+ "command": "npx",
41
+ "args": ["-y", "vyra-mcp", "serve"],
42
+ "env": { "VYRA_API_KEY": "vyra_sk_your_key_here" }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Environment
49
+
50
+ | Variable | Default | Purpose |
51
+ |----------|---------|---------|
52
+ | `VYRA_API_KEY` | — | Your `vyra_sk_…` key (required) |
53
+ | `VYRA_BASE_URL` | `https://creator-networks-sigma.vercel.app` | Override the API origin (local dev/testing) |
54
+
55
+ ## License
56
+
57
+ MIT
package/dist/api.js ADDED
@@ -0,0 +1,63 @@
1
+ // Tiny REST client for the Vyra public API (/api/v1). Every MCP tool calls
2
+ // through here so auth, base URL, and error handling live in one place.
3
+ //
4
+ // Config comes from the environment so the same binary works locally and in
5
+ // production:
6
+ // VYRA_API_KEY — the brand's `vyra_sk_…` key (required)
7
+ // VYRA_BASE_URL — API origin (optional; defaults to production)
8
+ // Current hosting is the Vercel preview domain; swap to the real domain at launch.
9
+ const DEFAULT_BASE_URL = "https://creator-networks-sigma.vercel.app";
10
+ // Resolve the API origin, trimming a trailing slash so path-joining is safe.
11
+ function baseUrl() {
12
+ const raw = process.env.VYRA_BASE_URL?.trim() || DEFAULT_BASE_URL;
13
+ return raw.replace(/\/+$/, "");
14
+ }
15
+ // The API key is required for every call; fail loudly if it's missing.
16
+ function apiKey() {
17
+ const key = process.env.VYRA_API_KEY?.trim();
18
+ if (!key) {
19
+ throw new Error("VYRA_API_KEY is not set. Run `npx vyra-mcp connect <your-key>` first.");
20
+ }
21
+ return key;
22
+ }
23
+ // Make one authenticated request and unwrap the { data } / { error } envelope.
24
+ // Throws a readable Error on any non-2xx or transport failure — never returns
25
+ // a half-valid result.
26
+ export async function apiRequest(method, path, body) {
27
+ // Resolve config first so a missing key reports clearly (not as a network
28
+ // failure, which would be misleading).
29
+ const url = `${baseUrl()}/api/v1${path}`;
30
+ const key = apiKey();
31
+ let res;
32
+ try {
33
+ res = await fetch(url, {
34
+ method,
35
+ headers: {
36
+ Authorization: `Bearer ${key}`,
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: body === undefined ? undefined : JSON.stringify(body),
40
+ });
41
+ }
42
+ catch (err) {
43
+ const reason = err instanceof Error ? err.message : "network error";
44
+ throw new Error(`Could not reach Vyra at ${url}: ${reason}`);
45
+ }
46
+ // Parse JSON defensively — some error paths may return an empty body.
47
+ let json = null;
48
+ const text = await res.text();
49
+ if (text) {
50
+ try {
51
+ json = JSON.parse(text);
52
+ }
53
+ catch {
54
+ throw new Error(`Vyra returned a non-JSON response (HTTP ${res.status})`);
55
+ }
56
+ }
57
+ if (!res.ok) {
58
+ const message = json?.error ??
59
+ `HTTP ${res.status} from Vyra`;
60
+ throw new Error(message);
61
+ }
62
+ return json?.data;
63
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Entry point for the `vyra-mcp` binary.
3
+ //
4
+ // vyra-mcp connect <vyra_sk_…> [--base-url <url>] wire into Claude Desktop
5
+ // vyra-mcp serve run the MCP server (stdio)
6
+ // vyra-mcp same as `serve`
7
+ //
8
+ // `serve` is the default so MCP clients can spawn the bare binary.
9
+ import { connect } from "./connect.js";
10
+ import { serve } from "./server.js";
11
+ function getFlag(args, name) {
12
+ const i = args.indexOf(name);
13
+ return i >= 0 ? args[i + 1] : undefined;
14
+ }
15
+ async function main() {
16
+ const [command, ...rest] = process.argv.slice(2);
17
+ switch (command) {
18
+ case "connect": {
19
+ const key = rest.find((a) => !a.startsWith("--"));
20
+ if (!key) {
21
+ console.error("Usage: npx vyra-mcp connect <your-api-key>");
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ connect(key, getFlag(rest, "--base-url"));
26
+ return;
27
+ }
28
+ case undefined:
29
+ case "serve":
30
+ await serve();
31
+ return;
32
+ default:
33
+ console.error(`Unknown command: ${command}`);
34
+ console.error("Usage: npx vyra-mcp <connect|serve>");
35
+ process.exitCode = 1;
36
+ }
37
+ }
38
+ main().catch((err) => {
39
+ console.error(err instanceof Error ? err.message : err);
40
+ process.exit(1);
41
+ });
@@ -0,0 +1,98 @@
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");
18
+ }
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");
37
+ }
38
+ }
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) {
60
+ const trimmed = apiKey.trim();
61
+ if (!trimmed || !trimmed.startsWith("vyra_sk_")) {
62
+ console.error("That doesn't look like a Vyra key. Create one in Settings → Developers (it starts with vyra_sk_).");
63
+ process.exitCode = 1;
64
+ return;
65
+ }
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);
74
+ return;
75
+ }
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);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
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.");
98
+ }
package/dist/server.js ADDED
@@ -0,0 +1,127 @@
1
+ // The Vyra MCP server. Exposes the public REST API as MCP tools so an LLM agent
2
+ // (Claude, ChatGPT, any MCP client) can run creator campaigns in plain English.
3
+ //
4
+ // Each tool is a thin wrapper over one /api/v1 endpoint. Money is handled in
5
+ // integer cents end-to-end; the platform margin is never exposed (PRD §18).
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import { apiRequest } from "./api.js";
10
+ // Wrap any tool body so a thrown error becomes a clean MCP error result instead
11
+ // of crashing the transport. Success returns the value as pretty JSON text.
12
+ async function run(fn) {
13
+ try {
14
+ const data = await fn();
15
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
16
+ }
17
+ catch (err) {
18
+ const message = err instanceof Error ? err.message : "Unknown error";
19
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
20
+ }
21
+ }
22
+ export function buildServer() {
23
+ const server = new McpServer({ name: "vyra", version: "0.1.0" });
24
+ // --- Wallet -------------------------------------------------------------
25
+ server.registerTool("get_wallet_balance", {
26
+ title: "Get wallet balance",
27
+ description: "Get the brand's available prepaid balance (in cents and USD). Use before activating a campaign to confirm funds.",
28
+ inputSchema: {},
29
+ }, () => run(() => apiRequest("GET", "/wallet")));
30
+ // --- Campaigns: read ----------------------------------------------------
31
+ server.registerTool("list_campaigns", {
32
+ title: "List campaigns",
33
+ description: "List all of the brand's campaigns with status, budget, and fee details.",
34
+ inputSchema: {},
35
+ }, () => run(() => apiRequest("GET", "/campaigns")));
36
+ server.registerTool("get_campaign", {
37
+ title: "Get campaign",
38
+ description: "Get one campaign by id, including its current status.",
39
+ inputSchema: {
40
+ campaign_id: z.string().describe("The campaign id"),
41
+ },
42
+ }, ({ campaign_id }) => run(() => apiRequest("GET", `/campaigns/${campaign_id}`)));
43
+ server.registerTool("list_campaign_slots", {
44
+ title: "List campaign creators",
45
+ description: "List the creators who joined a campaign, with each submission's status and view count.",
46
+ inputSchema: {
47
+ campaign_id: z.string().describe("The campaign id"),
48
+ },
49
+ }, ({ campaign_id }) => run(() => apiRequest("GET", `/campaigns/${campaign_id}/slots`)));
50
+ // --- Submissions --------------------------------------------------------
51
+ server.registerTool("get_submission", {
52
+ title: "Get submission",
53
+ description: "Get one creator submission's status, metrics (views/likes/comments/shares), quality score, cost, and watch-window progress.",
54
+ inputSchema: {
55
+ submission_id: z.string().describe("The submission id"),
56
+ },
57
+ }, ({ submission_id }) => run(() => apiRequest("GET", `/submissions/${submission_id}`)));
58
+ // --- Campaigns: write ---------------------------------------------------
59
+ server.registerTool("create_campaign", {
60
+ title: "Create campaign (draft)",
61
+ description: "Create a new draft campaign. It is NOT live yet — call activate_campaign after funding. All money fields are integer cents.",
62
+ inputSchema: {
63
+ name: z.string().describe("Campaign name"),
64
+ fixed_fee_cents: z
65
+ .number()
66
+ .int()
67
+ .describe("Guaranteed fee paid to each creator on approval, in cents"),
68
+ budget_cents: z
69
+ .number()
70
+ .int()
71
+ .describe("Total campaign budget in cents"),
72
+ objective: z.string().optional().describe("Short objective/goal"),
73
+ platform: z
74
+ .enum(["tiktok", "instagram", "youtube"])
75
+ .optional()
76
+ .describe("Target platform (default tiktok)"),
77
+ countries: z
78
+ .array(z.string())
79
+ .optional()
80
+ .describe("Allowed 2-letter country codes"),
81
+ bonus_cap_cents: z
82
+ .number()
83
+ .int()
84
+ .optional()
85
+ .describe("Max performance bonus per creator, in cents"),
86
+ bonus_type: z.enum(["milestone", "cpm", "none"]).optional(),
87
+ bonus_cpm_cents: z
88
+ .number()
89
+ .int()
90
+ .optional()
91
+ .describe("Bonus per 1000 views, in cents (when bonus_type is cpm)"),
92
+ min_followers: z.number().int().optional(),
93
+ watch_window_days: z
94
+ .number()
95
+ .int()
96
+ .optional()
97
+ .describe("Days to track views before the bonus settles"),
98
+ key_message: z
99
+ .string()
100
+ .optional()
101
+ .describe("The brief / what creators should communicate"),
102
+ hashtags: z.array(z.string()).optional(),
103
+ promo_link: z.string().optional(),
104
+ },
105
+ }, (args) => run(() => apiRequest("POST", "/campaigns", args)));
106
+ server.registerTool("activate_campaign", {
107
+ title: "Activate campaign",
108
+ description: "Activate a draft campaign: checks funds, reserves budget, and makes it live for creators. Fails if the wallet balance is too low.",
109
+ inputSchema: {
110
+ campaign_id: z.string().describe("The campaign id to activate"),
111
+ },
112
+ }, ({ campaign_id }) => run(() => apiRequest("POST", `/campaigns/${campaign_id}/activate`)));
113
+ server.registerTool("pause_campaign", {
114
+ title: "Pause campaign",
115
+ description: "Pause a live campaign so no new creators can join. In-progress submissions are unaffected.",
116
+ inputSchema: {
117
+ campaign_id: z.string().describe("The campaign id to pause"),
118
+ },
119
+ }, ({ campaign_id }) => run(() => apiRequest("POST", `/campaigns/${campaign_id}/pause`)));
120
+ return server;
121
+ }
122
+ // Start the server on stdio (how MCP clients spawn it as a child process).
123
+ export async function serve() {
124
+ const server = buildServer();
125
+ const transport = new StdioServerTransport();
126
+ await server.connect(transport);
127
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "vyra-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Drive Vyra from Claude, ChatGPT, or any MCP client — manage performance-based creator campaigns in plain English.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "vyra-mcp": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.12.0",
22
+ "zod": "^3.25.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.0",
26
+ "typescript": "^5.7.0"
27
+ }
28
+ }