@originalvoices/mcp-server 1.0.3 → 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -20
  3. package/dist/index.js +107 -74
  4. package/package.json +6 -4
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
  [![npm version](https://img.shields.io/npm/v/@originalvoices/mcp-server.svg)](https://www.npmjs.com/package/@originalvoices/mcp-server)
4
4
 
5
- An MCP (Model Context Protocol) server that provides access to [OriginalVoices](https://originalvoices.ai) AI twins for audience research.
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/ov-mcp-server/dist/index.js"],
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
- ### ask_twins
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
- Ask questions to a specific audience using OriginalVoices AI twins.
61
+ ### ask_twins
60
62
 
61
- **Parameters:**
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
- | `audience` | string | Description of the audience |
66
- | `questions` | string[] | Array of questions to ask the audience |
67
-
68
- **Example:**
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
- "When purchasing running shoes, what's most important to you?",
75
- "How often do you replace your running shoes?"
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
- ## Development
84
+ ### list_audiences
81
85
 
82
- ```bash
83
- # Install dependencies
84
- npm install
86
+ Lists your saved audiences. Pass an `id` to `ask_twins` as `audienceId`.
85
87
 
86
- # Build
87
- npm run build
88
+ ### list_studies
88
89
 
89
- # Watch mode
90
- npm run dev
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: "1.0.3",
8
+ version: "2.0.0",
9
9
  });
10
- const client = new OriginalVoices({
11
- apiKey: process.env.ORIGINALVOICES_API_KEY,
12
- });
13
- server.tool("ask_twins", "Ask questions to a specific audience using Original Voices AI twins. Use this for ad-hoc audience queries with a free-text audience description.", {
14
- audience: z
15
- .string()
16
- .min(1)
17
- .max(255)
18
- .describe("The target audience to query. Typically includes demographics such as age range, gender, and location. Examples: 'Women aged 35-55 in the US and UK', 'Gen Z men (18-25) in urban areas'."),
19
- questions: z
20
- .array(z.string().min(1).max(2048))
21
- .min(1)
22
- .max(15)
23
- .describe("The questions to ask the Digital Twins. Open-ended questions work best."),
24
- }, async ({ audience, questions }) => {
25
- const response = await client.ask.open({
26
- questions,
27
- audiencePrompt: audience,
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(response.data, null, 2),
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("ask_audience", "Ask questions to a previously saved audience by its ID. Use list_projects first to discover projects and their audiences.", {
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
- .describe("The audience ID to query. Obtain IDs from list_projects."),
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(z.string().min(1).max(2048))
86
+ .array(QuestionSchema)
45
87
  .min(1)
46
88
  .max(15)
47
- .describe("The questions to ask the Digital Twins. Open-ended questions work best."),
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("Optional natural language filter to select respondents. If omitted, all respondents are queried. Examples: 'Samsung users', 'women over 30 who worry about battery'."),
73
- questions: z
74
- .array(z.string().min(1).max(2048))
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(15)
77
- .describe("The questions to ask the Digital Twins. Open-ended questions work best."),
78
- }, async ({ projectId, filter, questions }) => {
79
- const response = await client.ask.project(projectId, {
80
- questions,
81
- filter,
82
- });
83
- return {
84
- content: [
85
- {
86
- type: "text",
87
- text: JSON.stringify(response.data, null, 2),
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("list_projects", "List all research projects in the user's organization. Each project contains survey questions and pre-defined audiences. Use this to discover projects before using ask_project or ask_audience.", {}, async () => {
93
- // SDK projects.list() has a bug (hits /v1/audiences), so use client.get directly
94
- const response = await client.get("/v1/projects");
95
- return {
96
- content: [
97
- {
98
- type: "text",
99
- text: JSON.stringify(response.data, null, 2),
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": "1.0.3",
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,7 +27,8 @@
26
27
  "license": "MIT",
27
28
  "repository": {
28
29
  "type": "git",
29
- "url": "https://github.com/ovlabs/mcp-server-originalvoices.git"
30
+ "url": "git+https://github.com/ovlabs/originalvoices-js.git",
31
+ "directory": "packages/mcp-server"
30
32
  },
31
33
  "engines": {
32
34
  "node": ">=20"
@@ -37,7 +39,7 @@
37
39
  ],
38
40
  "dependencies": {
39
41
  "@modelcontextprotocol/sdk": "^1.25.2",
40
- "originalvoices": "^0.14.1",
42
+ "originalvoices": "^1.0.0",
41
43
  "zod": "^4.3.5"
42
44
  },
43
45
  "devDependencies": {