@originalvoices/mcp-server 1.0.2 → 2.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/LICENSE +21 -0
- package/README.md +23 -20
- package/dist/index.js +107 -74
- package/package.json +8 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OriginalVoices
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@originalvoices/mcp-server)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A local (stdio) MCP server for [OriginalVoices](https://originalvoices.ai): ask Digital Twins of real people from any MCP client.
|
|
6
|
+
|
|
7
|
+
Most clients can connect to the hosted server at `https://api.originalvoices.ai/mcp` instead, with no install. See [Connect your assistant](https://docs.originalvoices.ai/connect).
|
|
6
8
|
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
@@ -43,7 +45,7 @@ Add to your `claude_desktop_config.json`:
|
|
|
43
45
|
"mcpServers": {
|
|
44
46
|
"originalvoices": {
|
|
45
47
|
"command": "node",
|
|
46
|
-
"args": ["/path/to/
|
|
48
|
+
"args": ["/path/to/originalvoices-js/packages/mcp-server/dist/index.js"],
|
|
47
49
|
"env": {
|
|
48
50
|
"ORIGINALVOICES_API_KEY": "your-api-key"
|
|
49
51
|
}
|
|
@@ -54,41 +56,42 @@ Add to your `claude_desktop_config.json`:
|
|
|
54
56
|
|
|
55
57
|
## Tools
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
Same tools as the hosted server at `https://api.originalvoices.ai/mcp`, which is the recommended way to connect if your client supports HTTP.
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
### ask_twins
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
Ask up to 15 questions to Digital Twins. Pass exactly one of `audience`, `audienceId` or `studyId`.
|
|
62
64
|
|
|
63
65
|
| Name | Type | Description |
|
|
64
66
|
|------|------|-------------|
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
| `questions` | array, required | A string is an open question. `{ question, choices, multiple? }` is a choice question with 2-20 options. |
|
|
68
|
+
| `audience` | string | Who to ask, in plain words |
|
|
69
|
+
| `audienceId` | string | A saved audience from `list_audiences` |
|
|
70
|
+
| `studyId` | string | A study from `list_studies`; its participants answer |
|
|
71
|
+
| `filter` | string | Only with `studyId`: narrow participants in plain words |
|
|
72
|
+
| `sampleSize` | integer | How many participants to ask |
|
|
69
73
|
|
|
70
74
|
```json
|
|
71
75
|
{
|
|
72
76
|
"audience": "UK, 18-30, fitness enthusiasts",
|
|
73
77
|
"questions": [
|
|
74
|
-
"
|
|
75
|
-
"
|
|
78
|
+
{ "question": "Which matters most in a running shoe?", "choices": ["Price", "Comfort", "Style"] },
|
|
79
|
+
"Why did you pick that?"
|
|
76
80
|
]
|
|
77
81
|
}
|
|
78
82
|
```
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
### list_audiences
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
# Install dependencies
|
|
84
|
-
npm install
|
|
86
|
+
Lists your saved audiences. Pass an `id` to `ask_twins` as `audienceId`.
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
npm run build
|
|
88
|
+
### list_studies
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
Lists your finished studies. Pass an `id` to `ask_twins` as `studyId`.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
See the [repo README](../../README.md).
|
|
92
95
|
|
|
93
96
|
## FAQ
|
|
94
97
|
|
package/dist/index.js
CHANGED
|
@@ -2,104 +2,137 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import OriginalVoices from "originalvoices";
|
|
5
|
+
import { OriginalVoices, OriginalVoicesError } from "originalvoices";
|
|
6
6
|
const server = new McpServer({
|
|
7
7
|
name: "originalvoices",
|
|
8
|
-
version: "
|
|
8
|
+
version: "2.0.0",
|
|
9
9
|
});
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.max(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
// Created on first use, so a missing key shows up as a tool error rather than a crash.
|
|
11
|
+
let client;
|
|
12
|
+
const ov = () => (client ??= new OriginalVoices());
|
|
13
|
+
// Same tools and descriptions as the hosted server at https://api.originalvoices.ai/mcp.
|
|
14
|
+
const QuestionSchema = z.union([
|
|
15
|
+
z.string().min(1).max(2048),
|
|
16
|
+
z.object({
|
|
17
|
+
question: z.string().min(1).max(2048),
|
|
18
|
+
choices: z
|
|
19
|
+
.array(z.string().min(1).max(255))
|
|
20
|
+
.min(2)
|
|
21
|
+
.max(20)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("2-20 distinct options. Omit for an open question."),
|
|
24
|
+
multiple: z.boolean().optional().describe("Allow several picks. Defaults to false."),
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
/** MCP questions to POST /v1/ask questions, with ids q1, q2, … in order. */
|
|
28
|
+
function toAskQuestions(questions) {
|
|
29
|
+
return questions.map((q, i) => {
|
|
30
|
+
const id = `q${i + 1}`;
|
|
31
|
+
if (typeof q === "string")
|
|
32
|
+
return { id, type: "open", question: q };
|
|
33
|
+
if (!q.choices)
|
|
34
|
+
return { id, type: "open", question: q.question };
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
type: "choice",
|
|
38
|
+
question: q.question,
|
|
39
|
+
choices: q.choices,
|
|
40
|
+
isMultipleChoice: q.multiple ?? false,
|
|
41
|
+
};
|
|
28
42
|
});
|
|
43
|
+
}
|
|
44
|
+
function result(data) {
|
|
45
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
46
|
+
}
|
|
47
|
+
function errorResult(error) {
|
|
29
48
|
return {
|
|
30
49
|
content: [
|
|
31
50
|
{
|
|
32
51
|
type: "text",
|
|
33
|
-
text: JSON.stringify(
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
error: error instanceof OriginalVoicesError ? error.code : "error",
|
|
54
|
+
message: error instanceof Error ? error.message : String(error),
|
|
55
|
+
}),
|
|
34
56
|
},
|
|
35
57
|
],
|
|
58
|
+
isError: true,
|
|
36
59
|
};
|
|
37
|
-
}
|
|
38
|
-
server.tool("
|
|
60
|
+
}
|
|
61
|
+
server.tool("ask_twins", [
|
|
62
|
+
"Ask questions to Digital Twins and get each Twin's individual answers.",
|
|
63
|
+
"Each Digital Twin represents a real person: it is built from their data, experiences and opinions, and owned and trained by that person.",
|
|
64
|
+
"Pass exactly one of: audience (who to ask, described in words — the default), audienceId (a saved or network audience from list_audiences) or studyId (a study from list_studies; each participant's Digital Twin answers with that person's whole study conversation in context).",
|
|
65
|
+
"Questions can be open (a plain string, answered in the Twin's own words) or choice (an object with choices, returning the share of Twins that picked each option). Each Twin answers all questions in order in one conversation, so an open question after a choice question can ask why they picked it. Every answer carries a low, medium or high confidence.",
|
|
66
|
+
"Answers are billed per Twin answer (Twins × questions).",
|
|
67
|
+
].join(" "), {
|
|
68
|
+
audience: z
|
|
69
|
+
.string()
|
|
70
|
+
.min(1)
|
|
71
|
+
.max(2048)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("The target audience to query, passed through exactly as the user describes it. Use this unless the user refers to a saved audience or a study. Typically includes demographics such as age range, gender, and location/country. Keep it separate from the questions: the audience describes WHO to ask, not WHAT they care about. Examples: 'Women aged 35-55 in the US and UK', 'Gen Z men (18-25) in urban areas'."),
|
|
39
74
|
audienceId: z
|
|
40
75
|
.string()
|
|
41
76
|
.min(1)
|
|
42
|
-
.
|
|
77
|
+
.max(256)
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("A saved or network audience to ask, instead of audience. Obtain IDs from list_audiences: a UUID for a saved audience, or '{networkId}:{audienceSlug}' for a network audience."),
|
|
80
|
+
studyId: z
|
|
81
|
+
.string()
|
|
82
|
+
.uuid()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("A study id from list_studies, instead of audience: its participants answer."),
|
|
43
85
|
questions: z
|
|
44
|
-
.array(
|
|
86
|
+
.array(QuestionSchema)
|
|
45
87
|
.min(1)
|
|
46
88
|
.max(15)
|
|
47
|
-
.describe("The questions to ask
|
|
48
|
-
}, async ({ audienceId, questions }) => {
|
|
49
|
-
const response = await client.ask.open({
|
|
50
|
-
questions,
|
|
51
|
-
audienceId,
|
|
52
|
-
});
|
|
53
|
-
return {
|
|
54
|
-
content: [
|
|
55
|
-
{
|
|
56
|
-
type: "text",
|
|
57
|
-
text: JSON.stringify(response.data, null, 2),
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
server.tool("ask_project", "Ask questions to a filtered subset of a project's survey panel. Filters respondents by demographics and survey answers using natural language. Use list_projects first to see available projects.", {
|
|
63
|
-
projectId: z
|
|
64
|
-
.string()
|
|
65
|
-
.min(1)
|
|
66
|
-
.describe("The project ID to query. Obtain IDs from list_projects."),
|
|
89
|
+
.describe("The questions to ask, 1-15, in order. An open question is a plain string. A choice question is an object { question, choices: 2-20 distinct options, multiple?: true to allow several picks }. Open questions work best for qualitative insights; use choice questions for numbers, shares or a preference between options."),
|
|
67
90
|
filter: z
|
|
68
91
|
.string()
|
|
69
92
|
.min(1)
|
|
70
93
|
.max(1024)
|
|
71
94
|
.optional()
|
|
72
|
-
.describe("
|
|
73
|
-
|
|
74
|
-
.
|
|
95
|
+
.describe("Only with studyId. Free-text criteria matched against each participant's profile and what they said in the study, e.g. 'said price was the main barrier'. Omit to ask everyone."),
|
|
96
|
+
sampleSize: z
|
|
97
|
+
.number()
|
|
98
|
+
.int()
|
|
75
99
|
.min(1)
|
|
76
|
-
.max(
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
100
|
+
.max(200)
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("How many participants to ask. Defaults: described audiences 25; saved audiences everyone up to 100; interview studies everyone; twin-survey studies 25. Audiences take at most 100."),
|
|
103
|
+
}, async ({ audience, audienceId, studyId, questions, filter, sampleSize }) => {
|
|
104
|
+
try {
|
|
105
|
+
return result(await ov().ask({
|
|
106
|
+
target: { audience, audienceId, studyId },
|
|
107
|
+
questions: toAskQuestions(questions),
|
|
108
|
+
filter,
|
|
109
|
+
sampleSize,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return errorResult(error);
|
|
114
|
+
}
|
|
91
115
|
});
|
|
92
|
-
server.tool("
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
server.tool("list_audiences", "List the saved audiences in the user's organization. Pass an entry's `id` to ask_twins as audienceId.", {}, async () => {
|
|
117
|
+
try {
|
|
118
|
+
return result(await ov().audiences.list());
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return errorResult(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.tool("list_studies", [
|
|
125
|
+
"List the organization's finished studies.",
|
|
126
|
+
"kind HUMAN_INTERVIEW: real people interviewed one-to-one by an AI moderator.",
|
|
127
|
+
"kind TWIN_SURVEY: a questionnaire answered by Digital Twins.",
|
|
128
|
+
"Each entry has the participant count and the study's questions. Pass the returned `id` to ask_twins as studyId.",
|
|
129
|
+
].join(" "), {}, async () => {
|
|
130
|
+
try {
|
|
131
|
+
return result(await ov().studies.list());
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return errorResult(error);
|
|
135
|
+
}
|
|
103
136
|
});
|
|
104
137
|
async function main() {
|
|
105
138
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@originalvoices/mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"mcpName": "io.github.ovlabs/originalvoices",
|
|
5
5
|
"description": "MCP server for OriginalVoices - Ask AI twins questions about any audience",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
|
-
"prepublishOnly": "npm run build"
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"mcp",
|
|
@@ -26,17 +27,19 @@
|
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/ovlabs/
|
|
30
|
+
"url": "git+https://github.com/ovlabs/originalvoices-js.git",
|
|
31
|
+
"directory": "packages/mcp-server"
|
|
30
32
|
},
|
|
31
33
|
"engines": {
|
|
32
34
|
"node": ">=20"
|
|
33
35
|
},
|
|
34
36
|
"files": [
|
|
35
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md"
|
|
36
39
|
],
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
39
|
-
"originalvoices": "^0.
|
|
42
|
+
"originalvoices": "^1.0.0",
|
|
40
43
|
"zod": "^4.3.5"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|