convai-character-mcp 1.0.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/.env.example +1 -0
- package/README.md +136 -0
- package/index.js +76 -0
- package/package.json +16 -0
- package/tools/character.js +225 -0
- package/tools/knowledge.js +132 -0
- package/tools/narrative.js +175 -0
- package/utils/api.js +52 -0
package/.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CONVAI_API_KEY=your_convai_api_key_here
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Convai MCP Server
|
|
2
|
+
|
|
3
|
+
Create and manage Convai AI characters directly from Claude — no dashboard needed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## For New Users: Quick Setup Guide
|
|
8
|
+
|
|
9
|
+
### Step 1 — Get your Convai API key
|
|
10
|
+
|
|
11
|
+
1. Go to [convai.com](https://convai.com) and sign in (or create a free account)
|
|
12
|
+
2. Open your dashboard and find **API Keys** in the settings/profile menu
|
|
13
|
+
3. Copy your API key — you'll need it in Step 3
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Step 2 — Find your Claude Desktop config file
|
|
18
|
+
|
|
19
|
+
**On Mac:**
|
|
20
|
+
|
|
21
|
+
1. Open **Finder**
|
|
22
|
+
2. Press `Cmd + Shift + G`
|
|
23
|
+
3. Paste this path and press Enter:
|
|
24
|
+
```
|
|
25
|
+
~/Library/Application Support/Claude/
|
|
26
|
+
```
|
|
27
|
+
4. Open the file named `claude_desktop_config.json` in a text editor (TextEdit, VS Code, etc.)
|
|
28
|
+
|
|
29
|
+
> If the file doesn't exist yet, create it with `{}` as the content first.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### Step 3 — Add the Convai MCP config
|
|
34
|
+
|
|
35
|
+
Paste this into `claude_desktop_config.json`, replacing `your_api_key_here` with the key from Step 1:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"convai": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "convai-mcp-server"],
|
|
43
|
+
"env": {
|
|
44
|
+
"CONVAI_API_KEY": "your_api_key_here"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> If the file already has other `mcpServers` entries, add the `"convai": { ... }` block inside the existing `mcpServers` object — don't replace the whole file.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Step 4 — Restart Claude Desktop
|
|
56
|
+
|
|
57
|
+
1. Fully **quit** Claude Desktop (right-click the dock icon → Quit, or use the menu bar → Quit)
|
|
58
|
+
2. Reopen Claude Desktop from your Applications folder or Dock
|
|
59
|
+
3. Open a new chat
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### Step 5 — Test it!
|
|
64
|
+
|
|
65
|
+
In a new Claude chat, type:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
List my Convai characters
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Claude will call the Convai API and show your characters. If you don't have any yet, it will say so — that means it's working!
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
**"I don't see any Convai tools"**
|
|
78
|
+
- Make sure you fully quit and reopened Claude Desktop (not just closed the window)
|
|
79
|
+
- Check that the JSON in your config file is valid — use [jsonlint.com](https://jsonlint.com) to verify
|
|
80
|
+
- Make sure your API key has no extra spaces
|
|
81
|
+
|
|
82
|
+
**"Error: CONVAI_API_KEY is not set"**
|
|
83
|
+
- Double-check that `"CONVAI_API_KEY"` is inside the `"env"` block in your config
|
|
84
|
+
|
|
85
|
+
**"API error 401 / Unauthorized"**
|
|
86
|
+
- Your API key may be incorrect or expired — regenerate it from the Convai dashboard
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## All Available Tools
|
|
91
|
+
|
|
92
|
+
| Tool | What it does | Required inputs |
|
|
93
|
+
|------|-------------|-----------------|
|
|
94
|
+
| `create_character` | Create a new AI character | `name`, `backstory` |
|
|
95
|
+
| `get_character` | Get full details of a character | `character_id` |
|
|
96
|
+
| `update_character` | Update name, backstory, or voice | `character_id` |
|
|
97
|
+
| `list_characters` | List all your characters | _(none)_ |
|
|
98
|
+
| `delete_character` | Delete a character | `character_id` |
|
|
99
|
+
| `add_knowledge` | Add knowledge to a character | `character_id`, `knowledge_text` |
|
|
100
|
+
| `list_knowledge` | List a character's knowledge entries | `character_id` |
|
|
101
|
+
| `set_narrative_design` | Set role, emotional range, speaking style | `character_id` |
|
|
102
|
+
| `clone_character` | Clone a character with a new name/backstory | `source_character_id`, `new_name` |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Example Prompts
|
|
107
|
+
|
|
108
|
+
Once connected, just describe what you want — Claude picks the right tool:
|
|
109
|
+
|
|
110
|
+
> "Create a wise elven librarian named Sylara with a female voice who speaks in archaic formal english"
|
|
111
|
+
|
|
112
|
+
> "Add knowledge about the Forbidden Archives to Sylara"
|
|
113
|
+
|
|
114
|
+
> "Give Sylara a narrative design as a Guide NPC with a calm to mysterious emotional range"
|
|
115
|
+
|
|
116
|
+
> "Clone Sylara into a new character called The Shadow Librarian with a darker, more paranoid personality"
|
|
117
|
+
|
|
118
|
+
> "List all my characters"
|
|
119
|
+
|
|
120
|
+
> "Delete the character with ID abc123"
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Prerequisites (for local/dev setup)
|
|
125
|
+
|
|
126
|
+
- Node.js 18+
|
|
127
|
+
- A Convai account and API key
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/noyonika25/convaimcp
|
|
131
|
+
cd convaimcp
|
|
132
|
+
npm install
|
|
133
|
+
cp .env.example .env
|
|
134
|
+
# Edit .env and add your CONVAI_API_KEY
|
|
135
|
+
node index.js
|
|
136
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
characterToolDefinitions,
|
|
12
|
+
handleCharacterTool,
|
|
13
|
+
} from "./tools/character.js";
|
|
14
|
+
import {
|
|
15
|
+
knowledgeToolDefinitions,
|
|
16
|
+
handleKnowledgeTool,
|
|
17
|
+
} from "./tools/knowledge.js";
|
|
18
|
+
import {
|
|
19
|
+
narrativeToolDefinitions,
|
|
20
|
+
handleNarrativeTool,
|
|
21
|
+
} from "./tools/narrative.js";
|
|
22
|
+
|
|
23
|
+
const allTools = [
|
|
24
|
+
...characterToolDefinitions,
|
|
25
|
+
...knowledgeToolDefinitions,
|
|
26
|
+
...narrativeToolDefinitions,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const server = new Server(
|
|
30
|
+
{ name: "convai-mcp-server", version: "1.0.0" },
|
|
31
|
+
{ capabilities: { tools: {} } }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
35
|
+
return { tools: allTools };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
39
|
+
const { name, arguments: args } = request.params;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Route by tool name prefix / ownership
|
|
43
|
+
if (name.startsWith("create_character") || name === "get_character" ||
|
|
44
|
+
name === "update_character" || name === "list_characters" ||
|
|
45
|
+
name === "delete_character") {
|
|
46
|
+
return await handleCharacterTool(name, args ?? {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (name === "add_knowledge" || name === "list_knowledge") {
|
|
50
|
+
return await handleKnowledgeTool(name, args ?? {});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (name === "set_narrative_design" || name === "clone_character") {
|
|
54
|
+
return await handleNarrativeTool(name, args ?? {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: `❌ Unknown tool: ${name}` }],
|
|
59
|
+
isError: true,
|
|
60
|
+
};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `❌ Error in ${name}: ${err.message}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const transport = new StdioServerTransport();
|
|
75
|
+
await server.connect(transport);
|
|
76
|
+
console.error("✅ Convai MCP server running");
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "convai-character-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Convai — create and manage AI characters from Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": { "convai-character-mcp": "./index.js" },
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node index.js",
|
|
10
|
+
"dev": "node --watch index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
14
|
+
"dotenv": "^16.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { convaiRequest } from "../utils/api.js";
|
|
2
|
+
|
|
3
|
+
export const characterToolDefinitions = [
|
|
4
|
+
{
|
|
5
|
+
name: "create_character",
|
|
6
|
+
description:
|
|
7
|
+
"Create a new Convai AI character with a name, backstory, voice type, and language.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
name: { type: "string", description: "Character name (required)" },
|
|
12
|
+
backstory: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Character backstory / personality description (required)",
|
|
15
|
+
},
|
|
16
|
+
voice_type: {
|
|
17
|
+
type: "string",
|
|
18
|
+
enum: ["MALE", "FEMALE"],
|
|
19
|
+
description: "Voice type — MALE or FEMALE (default: FEMALE)",
|
|
20
|
+
},
|
|
21
|
+
language: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: 'Language code (default: "en-US")',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["name", "backstory"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "get_character",
|
|
31
|
+
description: "Retrieve full details of a Convai character by ID.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
character_id: { type: "string", description: "The character ID" },
|
|
36
|
+
},
|
|
37
|
+
required: ["character_id"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "update_character",
|
|
42
|
+
description:
|
|
43
|
+
"Update an existing Convai character's name, backstory, or voice type.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
character_id: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "The character ID (required)",
|
|
50
|
+
},
|
|
51
|
+
name: { type: "string", description: "New character name (optional)" },
|
|
52
|
+
backstory: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "New backstory (optional)",
|
|
55
|
+
},
|
|
56
|
+
voice_type: {
|
|
57
|
+
type: "string",
|
|
58
|
+
enum: ["MALE", "FEMALE"],
|
|
59
|
+
description: "New voice type (optional)",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["character_id"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "list_characters",
|
|
67
|
+
description: "List all Convai characters in your account.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "delete_character",
|
|
75
|
+
description: "Delete a Convai character by ID.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
character_id: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "The character ID to delete",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["character_id"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export async function handleCharacterTool(name, args) {
|
|
90
|
+
switch (name) {
|
|
91
|
+
case "create_character":
|
|
92
|
+
return await createCharacter(args);
|
|
93
|
+
case "get_character":
|
|
94
|
+
return await getCharacter(args);
|
|
95
|
+
case "update_character":
|
|
96
|
+
return await updateCharacter(args);
|
|
97
|
+
case "list_characters":
|
|
98
|
+
return await listCharacters();
|
|
99
|
+
case "delete_character":
|
|
100
|
+
return await deleteCharacter(args);
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`Unknown character tool: ${name}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function createCharacter({ name, backstory, voice_type = "FEMALE", language = "en-US" }) {
|
|
107
|
+
// Convai returns: { charID, status } — exact shape may vary
|
|
108
|
+
const data = await convaiRequest("POST", "/v1/character/create", {
|
|
109
|
+
charName: name,
|
|
110
|
+
backstory,
|
|
111
|
+
voiceType: voice_type,
|
|
112
|
+
languageCode: language,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const charId = data?.charID ?? data?.character_id ?? data?.id ?? "unknown";
|
|
116
|
+
const status = data?.status ?? "created";
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: `✅ Character created successfully!\n\n**Name:** ${name}\n**ID:** ${charId}\n**Voice:** ${voice_type}\n**Language:** ${language}\n**Status:** ${status}\n\n**Next steps:**\n- Use \`add_knowledge\` to give ${name} specialized knowledge\n- Use \`set_narrative_design\` to define their role and speaking style\n- Use \`get_character\` to review the full character profile`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getCharacter({ character_id }) {
|
|
129
|
+
// Convai returns: full character object — exact shape may vary
|
|
130
|
+
const data = await convaiRequest("GET", `/v1/character/get?charID=${character_id}`);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: `**Character Details (ID: ${character_id})**\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function updateCharacter({ character_id, name, backstory, voice_type }) {
|
|
143
|
+
const updates = { charID: character_id };
|
|
144
|
+
const updated = [];
|
|
145
|
+
|
|
146
|
+
if (name !== undefined) {
|
|
147
|
+
updates.charName = name;
|
|
148
|
+
updated.push(`name → "${name}"`);
|
|
149
|
+
}
|
|
150
|
+
if (backstory !== undefined) {
|
|
151
|
+
updates.backstory = backstory;
|
|
152
|
+
updated.push("backstory");
|
|
153
|
+
}
|
|
154
|
+
if (voice_type !== undefined) {
|
|
155
|
+
updates.voiceType = voice_type;
|
|
156
|
+
updated.push(`voice_type → ${voice_type}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (updated.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: "⚠️ No fields provided to update." }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Convai returns: { status } or similar — exact shape may vary
|
|
166
|
+
const data = await convaiRequest("POST", "/v1/character/update", updates);
|
|
167
|
+
|
|
168
|
+
const status = data?.status ?? "updated";
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `✅ Character updated (ID: ${character_id})\n\n**Updated:** ${updated.join(", ")}\n**Status:** ${status}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function listCharacters() {
|
|
181
|
+
// Convai returns: array or object containing characters — exact shape may vary
|
|
182
|
+
const data = await convaiRequest("GET", "/v1/characters");
|
|
183
|
+
|
|
184
|
+
const characters = Array.isArray(data)
|
|
185
|
+
? data
|
|
186
|
+
: data?.characters ?? data?.data ?? [];
|
|
187
|
+
|
|
188
|
+
if (characters.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: "No characters found in your account." }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const list = characters
|
|
195
|
+
.map((c, i) => {
|
|
196
|
+
const id = c?.charID ?? c?.character_id ?? c?.id ?? "unknown";
|
|
197
|
+
const name = c?.charName ?? c?.name ?? "Unnamed";
|
|
198
|
+
const voice = c?.voiceType ?? c?.voice_type ?? "—";
|
|
199
|
+
return `${i + 1}. **${name}** — ID: \`${id}\` | Voice: ${voice}`;
|
|
200
|
+
})
|
|
201
|
+
.join("\n");
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `**Your Convai Characters (${characters.length} total)**\n\n${list}`,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function deleteCharacter({ character_id }) {
|
|
214
|
+
// Convai returns: empty or { status } — exact shape may vary
|
|
215
|
+
await convaiRequest("DELETE", `/v1/character/${character_id}`);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: `✅ Character \`${character_id}\` has been deleted.`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { convaiRequest } from "../utils/api.js";
|
|
2
|
+
|
|
3
|
+
export const knowledgeToolDefinitions = [
|
|
4
|
+
{
|
|
5
|
+
name: "add_knowledge",
|
|
6
|
+
description:
|
|
7
|
+
"Add a knowledge entry to a Convai character's knowledge bank.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
character_id: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "The character ID to add knowledge to (required)",
|
|
14
|
+
},
|
|
15
|
+
knowledge_text: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The knowledge content to add (required)",
|
|
18
|
+
},
|
|
19
|
+
knowledge_title: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "A short title for this knowledge entry (optional)",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["character_id", "knowledge_text"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "list_knowledge",
|
|
29
|
+
description: "List all knowledge entries for a Convai character.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
character_id: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "The character ID to list knowledge for (required)",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["character_id"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export async function handleKnowledgeTool(name, args) {
|
|
44
|
+
switch (name) {
|
|
45
|
+
case "add_knowledge":
|
|
46
|
+
return await addKnowledge(args);
|
|
47
|
+
case "list_knowledge":
|
|
48
|
+
return await listKnowledge(args);
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unknown knowledge tool: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function addKnowledge({ character_id, knowledge_text, knowledge_title }) {
|
|
55
|
+
const body = {
|
|
56
|
+
charID: character_id,
|
|
57
|
+
knowledge: knowledge_text,
|
|
58
|
+
};
|
|
59
|
+
if (knowledge_title !== undefined) {
|
|
60
|
+
body.title = knowledge_title;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Convai returns: { status, knowledgeID } or similar — exact shape may vary
|
|
64
|
+
const data = await convaiRequest("POST", "/v1/knowledge/upload", body);
|
|
65
|
+
|
|
66
|
+
const status = data?.status ?? "uploaded";
|
|
67
|
+
const knowledgeId = data?.knowledgeID ?? data?.knowledge_id ?? data?.id;
|
|
68
|
+
|
|
69
|
+
const preview =
|
|
70
|
+
knowledge_text.length > 120
|
|
71
|
+
? knowledge_text.slice(0, 120) + "…"
|
|
72
|
+
: knowledge_text;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: [
|
|
79
|
+
`✅ Knowledge added to character \`${character_id}\``,
|
|
80
|
+
knowledge_title ? `**Title:** ${knowledge_title}` : null,
|
|
81
|
+
knowledgeId ? `**Knowledge ID:** ${knowledgeId}` : null,
|
|
82
|
+
`**Status:** ${status}`,
|
|
83
|
+
`**Preview:** "${preview}"`,
|
|
84
|
+
]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("\n"),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function listKnowledge({ character_id }) {
|
|
93
|
+
// Convai returns: array or object of knowledge entries — exact shape may vary
|
|
94
|
+
const data = await convaiRequest(
|
|
95
|
+
"GET",
|
|
96
|
+
`/v1/knowledge/list?charID=${character_id}`
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const entries = Array.isArray(data)
|
|
100
|
+
? data
|
|
101
|
+
: data?.knowledge ?? data?.entries ?? data?.data ?? [];
|
|
102
|
+
|
|
103
|
+
if (entries.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `No knowledge entries found for character \`${character_id}\`.`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const list = entries
|
|
115
|
+
.map((e, i) => {
|
|
116
|
+
const id = e?.knowledgeID ?? e?.knowledge_id ?? e?.id ?? "unknown";
|
|
117
|
+
const title = e?.title ?? e?.name ?? "Untitled";
|
|
118
|
+
const preview = (e?.knowledge ?? e?.text ?? e?.content ?? "")
|
|
119
|
+
.slice(0, 80);
|
|
120
|
+
return `${i + 1}. **${title}** (ID: \`${id}\`)\n ${preview}${preview.length === 80 ? "…" : ""}`;
|
|
121
|
+
})
|
|
122
|
+
.join("\n\n");
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: `**Knowledge entries for character \`${character_id}\` (${entries.length} total)**\n\n${list}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { convaiRequest } from "../utils/api.js";
|
|
2
|
+
|
|
3
|
+
export const narrativeToolDefinitions = [
|
|
4
|
+
{
|
|
5
|
+
name: "set_narrative_design",
|
|
6
|
+
description:
|
|
7
|
+
"Set the narrative design of a Convai character — their role classification, emotional range, speaking style, and key topics of expertise. This appends a structured addendum to the character's backstory.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
character_id: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "The character ID (required)",
|
|
14
|
+
},
|
|
15
|
+
classification: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: 'Character role, e.g. "NPC", "Guide", "Antagonist"',
|
|
18
|
+
},
|
|
19
|
+
emotional_range: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: 'Emotional range, e.g. "calm to aggressive"',
|
|
22
|
+
},
|
|
23
|
+
speaking_style: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: 'Speaking style, e.g. "formal", "casual", "archaic"',
|
|
26
|
+
},
|
|
27
|
+
key_topics: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: { type: "string" },
|
|
30
|
+
description: "Topics this character is an expert in",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
required: ["character_id"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "clone_character",
|
|
38
|
+
description:
|
|
39
|
+
"Clone an existing Convai character into a new character with the same voice settings, optionally overriding the backstory.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
source_character_id: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "ID of the character to clone (required)",
|
|
46
|
+
},
|
|
47
|
+
new_name: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Name for the new character (required)",
|
|
50
|
+
},
|
|
51
|
+
backstory_override: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description:
|
|
54
|
+
"If provided, use this backstory instead of the source character's (optional)",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ["source_character_id", "new_name"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export async function handleNarrativeTool(name, args) {
|
|
63
|
+
switch (name) {
|
|
64
|
+
case "set_narrative_design":
|
|
65
|
+
return await setNarrativeDesign(args);
|
|
66
|
+
case "clone_character":
|
|
67
|
+
return await cloneCharacter(args);
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`Unknown narrative tool: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function setNarrativeDesign({
|
|
74
|
+
character_id,
|
|
75
|
+
classification,
|
|
76
|
+
emotional_range,
|
|
77
|
+
speaking_style,
|
|
78
|
+
key_topics,
|
|
79
|
+
}) {
|
|
80
|
+
// Fetch existing character to preserve current backstory
|
|
81
|
+
// Convai returns: full character object — exact shape may vary
|
|
82
|
+
const existing = await convaiRequest(
|
|
83
|
+
"GET",
|
|
84
|
+
`/v1/character/get?charID=${character_id}`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const currentBackstory =
|
|
88
|
+
existing?.backstory ?? existing?.charBackstory ?? existing?.description ?? "";
|
|
89
|
+
|
|
90
|
+
// Build the narrative addendum
|
|
91
|
+
const addendumLines = ["--- Narrative Design ---"];
|
|
92
|
+
if (classification) addendumLines.push(`Role: ${classification}`);
|
|
93
|
+
if (emotional_range) addendumLines.push(`Emotional range: ${emotional_range}`);
|
|
94
|
+
if (speaking_style) addendumLines.push(`Speaking style: ${speaking_style}`);
|
|
95
|
+
if (key_topics && key_topics.length > 0) {
|
|
96
|
+
addendumLines.push(`Key expertise: ${key_topics.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
addendumLines.push("--- End Narrative Design ---");
|
|
99
|
+
|
|
100
|
+
const addendum = addendumLines.join("\n");
|
|
101
|
+
|
|
102
|
+
// Remove any previous narrative addendum before appending new one
|
|
103
|
+
const backstoryWithoutOldAddendum = currentBackstory
|
|
104
|
+
.replace(/\n?--- Narrative Design ---[\s\S]*?--- End Narrative Design ---/g, "")
|
|
105
|
+
.trim();
|
|
106
|
+
|
|
107
|
+
const newBackstory = backstoryWithoutOldAddendum
|
|
108
|
+
? `${backstoryWithoutOldAddendum}\n\n${addendum}`
|
|
109
|
+
: addendum;
|
|
110
|
+
|
|
111
|
+
// Convai returns: { status } or similar — exact shape may vary
|
|
112
|
+
const data = await convaiRequest("POST", "/v1/character/update", {
|
|
113
|
+
charID: character_id,
|
|
114
|
+
backstory: newBackstory,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const status = data?.status ?? "updated";
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: `✅ Narrative design applied to character \`${character_id}\`\n**Status:** ${status}\n\n**Addendum added to backstory:**\n\`\`\`\n${addendum}\n\`\`\``,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cloneCharacter({ source_character_id, new_name, backstory_override }) {
|
|
130
|
+
// Fetch source character
|
|
131
|
+
// Convai returns: full character object — exact shape may vary
|
|
132
|
+
const source = await convaiRequest(
|
|
133
|
+
"GET",
|
|
134
|
+
`/v1/character/get?charID=${source_character_id}`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const voiceType =
|
|
138
|
+
source?.voiceType ?? source?.voice_type ?? "FEMALE";
|
|
139
|
+
const languageCode =
|
|
140
|
+
source?.languageCode ?? source?.language_code ?? source?.language ?? "en-US";
|
|
141
|
+
const backstory =
|
|
142
|
+
backstory_override ??
|
|
143
|
+
source?.backstory ??
|
|
144
|
+
source?.charBackstory ??
|
|
145
|
+
source?.description ??
|
|
146
|
+
"";
|
|
147
|
+
|
|
148
|
+
// Create the new character
|
|
149
|
+
// Convai returns: { charID, status } — exact shape may vary
|
|
150
|
+
const newChar = await convaiRequest("POST", "/v1/character/create", {
|
|
151
|
+
charName: new_name,
|
|
152
|
+
backstory,
|
|
153
|
+
voiceType,
|
|
154
|
+
languageCode,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const newCharId = newChar?.charID ?? newChar?.character_id ?? newChar?.id ?? "unknown";
|
|
158
|
+
const status = newChar?.status ?? "created";
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: [
|
|
165
|
+
`✅ Character cloned successfully!`,
|
|
166
|
+
`**Source:** \`${source_character_id}\``,
|
|
167
|
+
`**New character:** ${new_name} (ID: \`${newCharId}\`)`,
|
|
168
|
+
`**Voice:** ${voiceType} | **Language:** ${languageCode}`,
|
|
169
|
+
`**Backstory:** ${backstory_override ? "custom override applied" : "copied from source"}`,
|
|
170
|
+
`**Status:** ${status}`,
|
|
171
|
+
].join("\n"),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
package/utils/api.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reusable Convai API client.
|
|
5
|
+
* @param {string} method - HTTP method (GET, POST, DELETE, etc.)
|
|
6
|
+
* @param {string} path - API path, e.g. "/v1/character/create"
|
|
7
|
+
* @param {object} [body] - Optional request body (will be JSON-encoded)
|
|
8
|
+
* @returns {Promise<any>} Parsed JSON response
|
|
9
|
+
*/
|
|
10
|
+
export async function convaiRequest(method, path, body) {
|
|
11
|
+
const apiKey = process.env.CONVAI_API_KEY;
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
throw new Error("CONVAI_API_KEY is not set in environment variables");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const url = `https://api.convai.com${path}`;
|
|
17
|
+
const options = {
|
|
18
|
+
method,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"CONVAI-API-KEY": apiKey,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (body !== undefined) {
|
|
26
|
+
options.body = JSON.stringify(body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = await fetch(url, options);
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
let errorBody = "";
|
|
33
|
+
try {
|
|
34
|
+
errorBody = await res.text();
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Convai API error ${res.status} ${res.statusText}: ${errorBody}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Some endpoints may return empty body (e.g. DELETE)
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
if (!text) return {};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
return { raw: text };
|
|
51
|
+
}
|
|
52
|
+
}
|