vyra-mcp 0.3.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/README.md +10 -2
- package/dist/api.js +6 -0
- package/dist/cli.js +11 -3
- package/dist/clients.js +122 -0
- package/dist/connect.js +53 -83
- package/dist/server.js +116 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,15 +6,23 @@ 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
|
|
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
|
|
|
20
28
|
On connect, the server tells your agent how Vyra works (performance-based creator
|
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/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>]
|
|
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,
|
|
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:
|
package/dist/clients.js
ADDED
|
@@ -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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
@@ -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.
|
|
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