@clien-ai/mcp 0.1.3 → 0.1.5
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 +19 -0
- package/dist/config.js +10 -2
- package/dist/config.js.map +1 -1
- package/dist/server.js +26 -143
- package/dist/server.js.map +1 -1
- package/dist/tools/errors.js +23 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/interview.js +263 -0
- package/dist/tools/interview.js.map +1 -0
- package/dist/tools/personas.js +192 -0
- package/dist/tools/personas.js.map +1 -0
- package/dist/tools/projects.js +195 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/registry.js +147 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/reports.js +210 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/research.js +45 -11
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/schema.js +25 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/status.js +107 -13
- package/dist/tools/status.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create_persona` — resource primitive over the Phase-0 personas REST API
|
|
3
|
+
* (`POST /api/personas`, built by FUL-16). Creates a persona row from explicit
|
|
4
|
+
* fields the caller already knows.
|
|
5
|
+
*
|
|
6
|
+
* The MCP package is a thin bearer-auth HTTP client: this tool reuses the
|
|
7
|
+
* shared `apiCall` client from `research.ts` (same 401→refresh→retry behavior,
|
|
8
|
+
* per-endpoint timeouts) exactly as `projects.ts` and `status.ts` do. No new
|
|
9
|
+
* HTTP machinery.
|
|
10
|
+
*
|
|
11
|
+
* Contract:
|
|
12
|
+
* - create_persona → POST /api/personas
|
|
13
|
+
* { projectId, name, role, company?, description?, companySize?,
|
|
14
|
+
* industry?, location?, ephemeral? } → 201 { persona }.
|
|
15
|
+
* Returns a confirmation line + the persona in `_meta.persona`.
|
|
16
|
+
* The backend stamps `source='mcp'` automatically for bearer callers
|
|
17
|
+
* (FUL-21). `ephemeral: true` sets `include_in_insights=false` so a
|
|
18
|
+
* scratch persona is excluded from the project's aggregate insights.
|
|
19
|
+
*
|
|
20
|
+
* Scope note: this covers the explicit-field (free) create path only. The
|
|
21
|
+
* AI generate/enrich mode with a per-tool credit debit is deferred — its
|
|
22
|
+
* infrastructure (per-tool spend RPC, credit cost table, bearer auth on the AI
|
|
23
|
+
* generation routes) is not yet built. See the FUL-24 follow-up issue.
|
|
24
|
+
*
|
|
25
|
+
* Error mapping mirrors `projects.ts`: 401 → invalidate the cached session and
|
|
26
|
+
* surface a re-auth message; 404 → the project isn't owned by the caller;
|
|
27
|
+
* 400 → the server's validation message; other non-ok → a generic "HTTP <n>"
|
|
28
|
+
* error.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { ToolError } from './errors.js';
|
|
32
|
+
import { apiCall, categorizeFetchError, truncate, } from './research.js';
|
|
33
|
+
const PERSONAS_FETCH_TIMEOUT_MS = 30_000;
|
|
34
|
+
// Caps re-declared locally: the @clien-ai/mcp package is published separately
|
|
35
|
+
// and cannot import from the Next.js app. Kept in sync with `CreatePersonaSchema`
|
|
36
|
+
// in app/api/personas/route.ts (name/role/company/location max 200, description
|
|
37
|
+
// max 5000, companySize/industry max 100). The server re-validates regardless;
|
|
38
|
+
// this is the advertised spec.
|
|
39
|
+
const NAME_MAX = 200;
|
|
40
|
+
const ROLE_MAX = 200;
|
|
41
|
+
const COMPANY_MAX = 200;
|
|
42
|
+
const DESCRIPTION_MAX = 5000;
|
|
43
|
+
const COMPANY_SIZE_MAX = 100;
|
|
44
|
+
const INDUSTRY_MAX = 100;
|
|
45
|
+
const LOCATION_MAX = 200;
|
|
46
|
+
export const CreatePersonaInputSchema = z.object({
|
|
47
|
+
projectId: z
|
|
48
|
+
.string()
|
|
49
|
+
.uuid()
|
|
50
|
+
.describe('UUID of the project this persona belongs to. Get it from `list_projects` ' +
|
|
51
|
+
'(or `create_project`, whose `_meta.project.id` you can pass straight in). ' +
|
|
52
|
+
'The persona is rejected with a 404 if you do not own the project.'),
|
|
53
|
+
name: z
|
|
54
|
+
.string()
|
|
55
|
+
.trim()
|
|
56
|
+
.min(1, 'name must be non-empty')
|
|
57
|
+
.max(NAME_MAX, `name too long (max ${NAME_MAX} chars)`)
|
|
58
|
+
.describe('The persona\'s name (e.g. "Priya, the pragmatic PM"). Short and human-readable.'),
|
|
59
|
+
role: z
|
|
60
|
+
.string()
|
|
61
|
+
.trim()
|
|
62
|
+
.min(1, 'role must be non-empty')
|
|
63
|
+
.max(ROLE_MAX, `role too long (max ${ROLE_MAX} chars)`)
|
|
64
|
+
.describe('The persona\'s job title or role (e.g. "Senior Product Manager"). Required.'),
|
|
65
|
+
company: z
|
|
66
|
+
.string()
|
|
67
|
+
.max(COMPANY_MAX, `company too long (max ${COMPANY_MAX} chars)`)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Optional company or organisation the persona works at.'),
|
|
70
|
+
description: z
|
|
71
|
+
.string()
|
|
72
|
+
.max(DESCRIPTION_MAX, `description too long (max ${DESCRIPTION_MAX} chars)`)
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Optional background/bio for the persona: goals, pain points, context — ' +
|
|
75
|
+
'whatever helps an interview feel grounded. Stored as the persona\'s background.'),
|
|
76
|
+
companySize: z
|
|
77
|
+
.string()
|
|
78
|
+
.max(COMPANY_SIZE_MAX, `companySize too long (max ${COMPANY_SIZE_MAX} chars)`)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe('Optional company size (e.g. "11-50", "Enterprise").'),
|
|
81
|
+
industry: z
|
|
82
|
+
.string()
|
|
83
|
+
.max(INDUSTRY_MAX, `industry too long (max ${INDUSTRY_MAX} chars)`)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Optional industry (e.g. "Fintech", "Healthcare").'),
|
|
86
|
+
location: z
|
|
87
|
+
.string()
|
|
88
|
+
.max(LOCATION_MAX, `location too long (max ${LOCATION_MAX} chars)`)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Optional location (e.g. "Berlin, Germany").'),
|
|
91
|
+
ephemeral: z
|
|
92
|
+
.boolean()
|
|
93
|
+
.optional()
|
|
94
|
+
.default(false)
|
|
95
|
+
.describe('Set true for a scratch/throwaway persona that should NOT count toward the ' +
|
|
96
|
+
'project\'s aggregate insights (sets include_in_insights=false). Leave false ' +
|
|
97
|
+
'(the default) for a real persona you want reflected in project-level analysis.'),
|
|
98
|
+
});
|
|
99
|
+
export class PersonaToolError extends ToolError {
|
|
100
|
+
}
|
|
101
|
+
function formatZodError(error) {
|
|
102
|
+
return error.issues.map((i) => `${i.path.join('.') || 'input'}: ${i.message}`).join('; ');
|
|
103
|
+
}
|
|
104
|
+
function makeCtx(deps) {
|
|
105
|
+
return { config: deps.config, session: deps.session, fetch: deps.fetch ?? fetch };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Shared non-ok handling: 401 drops the cached session (so the next tool call
|
|
109
|
+
* rebuilds from disk) and surfaces a re-auth message; 404 means the caller does
|
|
110
|
+
* not own the target project; 400 surfaces the server's validation detail;
|
|
111
|
+
* everything else surfaces a generic HTTP error.
|
|
112
|
+
*/
|
|
113
|
+
async function throwForResponse(response, deps, action) {
|
|
114
|
+
const category = categorizeFetchError(response.status);
|
|
115
|
+
if (category === 'auth') {
|
|
116
|
+
if (response.refreshError?.kind !== 'transient')
|
|
117
|
+
deps.invalidateSession?.();
|
|
118
|
+
throw new PersonaToolError('Session expired. Re-run any Clien.ai tool to re-authenticate, then try again.');
|
|
119
|
+
}
|
|
120
|
+
if (response.status === 404) {
|
|
121
|
+
throw new PersonaToolError(`Could not ${action} — project not found, or you don't own it. ` +
|
|
122
|
+
'Call list_projects to confirm the projectId.');
|
|
123
|
+
}
|
|
124
|
+
const text = await response.text().catch(() => '');
|
|
125
|
+
let detail = truncate(text, 200);
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(text);
|
|
128
|
+
if (parsed?.error)
|
|
129
|
+
detail = `${parsed.error}${parsed.code ? ` (${parsed.code})` : ''}`;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// non-JSON body — fall back to the truncated raw text
|
|
133
|
+
}
|
|
134
|
+
if (response.status === 400) {
|
|
135
|
+
throw new PersonaToolError(`Could not ${action} — ${detail}`);
|
|
136
|
+
}
|
|
137
|
+
throw new PersonaToolError(`Failed to ${action} (HTTP ${response.status}): ${detail}`);
|
|
138
|
+
}
|
|
139
|
+
export async function createPersona(input, deps) {
|
|
140
|
+
const parsed = CreatePersonaInputSchema.safeParse(input);
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
throw new PersonaToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
143
|
+
}
|
|
144
|
+
const { projectId, name, role, company, description, companySize, industry, location, ephemeral } = parsed.data;
|
|
145
|
+
const ctx = makeCtx(deps);
|
|
146
|
+
const body = { projectId, name, role, ephemeral };
|
|
147
|
+
if (company !== undefined)
|
|
148
|
+
body.company = company;
|
|
149
|
+
if (description !== undefined)
|
|
150
|
+
body.description = description;
|
|
151
|
+
if (companySize !== undefined)
|
|
152
|
+
body.companySize = companySize;
|
|
153
|
+
if (industry !== undefined)
|
|
154
|
+
body.industry = industry;
|
|
155
|
+
if (location !== undefined)
|
|
156
|
+
body.location = location;
|
|
157
|
+
let response;
|
|
158
|
+
try {
|
|
159
|
+
response = await apiCall(ctx, 'POST', '/api/personas', {
|
|
160
|
+
body,
|
|
161
|
+
timeoutMs: PERSONAS_FETCH_TIMEOUT_MS,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const e = err;
|
|
166
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
167
|
+
throw new PersonaToolError(`Creating the persona timed out after ${PERSONAS_FETCH_TIMEOUT_MS / 1000}s. It may or may not have been created — call list_projects / check the project before retrying.`);
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
if (!response.ok)
|
|
172
|
+
await throwForResponse(response, deps, 'create persona');
|
|
173
|
+
const json = (await response.json().catch(() => null));
|
|
174
|
+
const persona = json?.persona;
|
|
175
|
+
if (!persona?.id) {
|
|
176
|
+
throw new PersonaToolError('Persona creation returned no persona id; the operation may not have succeeded.');
|
|
177
|
+
}
|
|
178
|
+
const insightsNote = ephemeral
|
|
179
|
+
? ' Marked ephemeral — excluded from project insights.'
|
|
180
|
+
: '';
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: `Created persona "${persona.name ?? name}" (${persona.role ?? role}, id: ${persona.id}) ` +
|
|
186
|
+
`in project ${projectId}.${insightsNote}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
_meta: { persona },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=personas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personas.js","sourceRoot":"","sources":["../../src/tools/personas.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `list_projects` + `create_project` — resource primitives over the Phase-0
|
|
3
|
+
* projects REST API (`GET`/`POST /api/projects`, built by FUL-15).
|
|
4
|
+
*
|
|
5
|
+
* The MCP package is a thin bearer-auth HTTP client: these tools reuse the
|
|
6
|
+
* shared `apiCall` client from `research.ts` (same 401→refresh→retry behavior,
|
|
7
|
+
* per-endpoint timeouts) exactly as `status.ts` does. No new HTTP machinery.
|
|
8
|
+
*
|
|
9
|
+
* Contract:
|
|
10
|
+
* - list_projects → GET /api/projects?limit&offset →
|
|
11
|
+
* { projects: Project[], pagination: { limit, offset, total } }.
|
|
12
|
+
* Returns a readable list as text + the raw arrays in `_meta` so a host
|
|
13
|
+
* AI can pick a `project_id` to pass to `clien_research`.
|
|
14
|
+
* - create_project → POST /api/projects { name, description? } →
|
|
15
|
+
* 201 { project }. Returns a confirmation line + the project in `_meta`.
|
|
16
|
+
*
|
|
17
|
+
* Error mapping mirrors `status.ts`: 401 → invalidate the cached session and
|
|
18
|
+
* surface a re-auth message; 400 → the server's validation message; other
|
|
19
|
+
* non-ok → a generic "HTTP <n>" error. All read/writes are cheap and
|
|
20
|
+
* idempotent-ish (create is the only mutation), so timeouts/network errors
|
|
21
|
+
* surface raw for the caller to retry.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { ToolError } from './errors.js';
|
|
25
|
+
import { apiCall, categorizeFetchError, truncate, } from './research.js';
|
|
26
|
+
const PROJECTS_FETCH_TIMEOUT_MS = 30_000;
|
|
27
|
+
// Caps re-declared locally: the @clien-ai/mcp package is published separately
|
|
28
|
+
// and cannot import from the Next.js app. Kept in sync with VALIDATION_LIMITS
|
|
29
|
+
// (PROJECT_NAME_MAX=100, PROJECT_DESCRIPTION_MAX=1500) in
|
|
30
|
+
// app/lib/constants/app.ts — matches the precedent set for `project_name` in
|
|
31
|
+
// research.ts. The server re-validates regardless; this is the advertised spec.
|
|
32
|
+
const PROJECT_NAME_MAX = 100;
|
|
33
|
+
const PROJECT_DESCRIPTION_MAX = 1500;
|
|
34
|
+
const LIST_LIMIT_MAX = 100;
|
|
35
|
+
const LIST_LIMIT_DEFAULT = 20;
|
|
36
|
+
export const ListProjectsInputSchema = z.object({
|
|
37
|
+
limit: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(LIST_LIMIT_MAX)
|
|
42
|
+
.optional()
|
|
43
|
+
.default(LIST_LIMIT_DEFAULT)
|
|
44
|
+
.describe(`Maximum number of projects to return (1-${LIST_LIMIT_MAX}, default ${LIST_LIMIT_DEFAULT}). ` +
|
|
45
|
+
'Newest projects come first.'),
|
|
46
|
+
offset: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.min(0)
|
|
50
|
+
.optional()
|
|
51
|
+
.default(0)
|
|
52
|
+
.describe('Number of projects to skip before returning results (default 0). ' +
|
|
53
|
+
'Combine with `limit` to page through a large project list; the ' +
|
|
54
|
+
'total count is returned in `_meta.pagination.total`.'),
|
|
55
|
+
});
|
|
56
|
+
export const CreateProjectInputSchema = z.object({
|
|
57
|
+
name: z
|
|
58
|
+
.string()
|
|
59
|
+
.trim()
|
|
60
|
+
.min(1, 'name must be non-empty')
|
|
61
|
+
.max(PROJECT_NAME_MAX, `name too long (max ${PROJECT_NAME_MAX} chars)`)
|
|
62
|
+
.describe('Human-readable name for the new project. Keep it short (2-4 words) — ' +
|
|
63
|
+
'typically the product or idea name. Use `list_projects` first to ' +
|
|
64
|
+
'avoid creating a duplicate of an existing project.'),
|
|
65
|
+
description: z
|
|
66
|
+
.string()
|
|
67
|
+
.max(PROJECT_DESCRIPTION_MAX, `description too long (max ${PROJECT_DESCRIPTION_MAX} chars)`)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Optional longer description of what the project is about (max ' +
|
|
70
|
+
`${PROJECT_DESCRIPTION_MAX} chars). Omit if the name is self-explanatory.`),
|
|
71
|
+
});
|
|
72
|
+
export class ProjectToolError extends ToolError {
|
|
73
|
+
}
|
|
74
|
+
function formatZodError(error) {
|
|
75
|
+
return error.issues.map((i) => `${i.path.join('.') || 'input'}: ${i.message}`).join('; ');
|
|
76
|
+
}
|
|
77
|
+
function makeCtx(deps) {
|
|
78
|
+
return { config: deps.config, session: deps.session, fetch: deps.fetch ?? fetch };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Shared non-ok handling: 401 drops the cached session (so the next tool call
|
|
82
|
+
* rebuilds from disk) and surfaces a re-auth message; 400 surfaces the server's
|
|
83
|
+
* validation detail; everything else surfaces a generic HTTP error.
|
|
84
|
+
*/
|
|
85
|
+
async function throwForResponse(response, deps, action) {
|
|
86
|
+
const category = categorizeFetchError(response.status);
|
|
87
|
+
if (category === 'auth') {
|
|
88
|
+
if (response.refreshError?.kind !== 'transient')
|
|
89
|
+
deps.invalidateSession?.();
|
|
90
|
+
throw new ProjectToolError('Session expired. Re-run any Clien.ai tool to re-authenticate, then try again.');
|
|
91
|
+
}
|
|
92
|
+
const text = await response.text().catch(() => '');
|
|
93
|
+
let detail = truncate(text, 200);
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(text);
|
|
96
|
+
if (parsed?.error)
|
|
97
|
+
detail = `${parsed.error}${parsed.code ? ` (${parsed.code})` : ''}`;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// non-JSON body — fall back to the truncated raw text
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 400) {
|
|
103
|
+
throw new ProjectToolError(`Could not ${action} — ${detail}`);
|
|
104
|
+
}
|
|
105
|
+
throw new ProjectToolError(`Failed to ${action} (HTTP ${response.status}): ${detail}`);
|
|
106
|
+
}
|
|
107
|
+
export async function listProjects(input, deps) {
|
|
108
|
+
const parsed = ListProjectsInputSchema.safeParse(input);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
throw new ProjectToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
111
|
+
}
|
|
112
|
+
const { limit, offset } = parsed.data;
|
|
113
|
+
const ctx = makeCtx(deps);
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await apiCall(ctx, 'GET', `/api/projects?limit=${limit}&offset=${offset}`, {
|
|
117
|
+
timeoutMs: PROJECTS_FETCH_TIMEOUT_MS,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const e = err;
|
|
122
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
123
|
+
throw new ProjectToolError(`Listing projects timed out after ${PROJECTS_FETCH_TIMEOUT_MS / 1000}s. Try again in a moment.`);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
if (!response.ok)
|
|
128
|
+
await throwForResponse(response, deps, 'list projects');
|
|
129
|
+
const json = (await response.json().catch(() => null));
|
|
130
|
+
const projects = json?.projects ?? [];
|
|
131
|
+
const total = json?.pagination?.total ?? projects.length;
|
|
132
|
+
let text;
|
|
133
|
+
if (projects.length === 0) {
|
|
134
|
+
text =
|
|
135
|
+
offset > 0
|
|
136
|
+
? `No projects at offset ${offset} (total: ${total}).`
|
|
137
|
+
: 'No projects yet. Use create_project to make one, or run clien_research with a project_name.';
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const lines = projects.map((p) => {
|
|
141
|
+
const desc = p.description ? ` — ${truncate(p.description, 100)}` : '';
|
|
142
|
+
return `- ${p.name ?? '(unnamed)'} (id: ${p.id ?? 'unknown'})${desc}`;
|
|
143
|
+
});
|
|
144
|
+
const shown = offset + projects.length;
|
|
145
|
+
const more = shown < total ? `\n\n${total - shown} more not shown — page with offset=${shown}.` : '';
|
|
146
|
+
text = `Found ${total} project${total === 1 ? '' : 's'}:\n${lines.join('\n')}${more}`;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text }],
|
|
150
|
+
_meta: { projects, pagination: json?.pagination ?? { limit, offset, total } },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export async function createProject(input, deps) {
|
|
154
|
+
const parsed = CreateProjectInputSchema.safeParse(input);
|
|
155
|
+
if (!parsed.success) {
|
|
156
|
+
throw new ProjectToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
157
|
+
}
|
|
158
|
+
const { name, description } = parsed.data;
|
|
159
|
+
const ctx = makeCtx(deps);
|
|
160
|
+
const body = { name };
|
|
161
|
+
if (description !== undefined)
|
|
162
|
+
body.description = description;
|
|
163
|
+
let response;
|
|
164
|
+
try {
|
|
165
|
+
response = await apiCall(ctx, 'POST', '/api/projects', {
|
|
166
|
+
body,
|
|
167
|
+
timeoutMs: PROJECTS_FETCH_TIMEOUT_MS,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const e = err;
|
|
172
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
173
|
+
throw new ProjectToolError(`Creating the project timed out after ${PROJECTS_FETCH_TIMEOUT_MS / 1000}s. It may or may not have been created — call list_projects to check before retrying.`);
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
if (!response.ok)
|
|
178
|
+
await throwForResponse(response, deps, 'create project');
|
|
179
|
+
const json = (await response.json().catch(() => null));
|
|
180
|
+
const project = json?.project;
|
|
181
|
+
if (!project?.id) {
|
|
182
|
+
throw new ProjectToolError('Project creation returned no project id; the operation may not have succeeded.');
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: `Created project "${project.name ?? name}" (id: ${project.id}). ` +
|
|
189
|
+
'Pass this id as `project_id` to clien_research to attach research runs to it.',
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
_meta: { project },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/tools/projects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry — the single source of truth for the MCP tool surface.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is a self-contained `{ name, description, zodSchema, handler }`.
|
|
5
|
+
* `server.ts` iterates this array for BOTH `tools/list` (advertising the
|
|
6
|
+
* Zod-generated `jsonSchema`) and `tools/call` (dispatching to `handler`).
|
|
7
|
+
* Adding a tool is a one-line push here — no dispatch edits, no hand-rolled
|
|
8
|
+
* JSON Schema.
|
|
9
|
+
*
|
|
10
|
+
* `jsonSchema` is precomputed once at module load via `zodToInputSchema`.
|
|
11
|
+
* Handlers are thin arrows that call the imported tool function at call time
|
|
12
|
+
* (not captured references) so ESM live-bindings keep `vi.mock(...)` in the
|
|
13
|
+
* tests working.
|
|
14
|
+
*/
|
|
15
|
+
import { zodToInputSchema } from './schema.js';
|
|
16
|
+
import { ResearchInputSchema, runResearch } from './research.js';
|
|
17
|
+
import { StatusInputSchema, getResearchStatus } from './status.js';
|
|
18
|
+
import { ListProjectsInputSchema, CreateProjectInputSchema, listProjects, createProject, } from './projects.js';
|
|
19
|
+
import { CreatePersonaInputSchema, createPersona } from './personas.js';
|
|
20
|
+
import { ListReportsInputSchema, GetReportInputSchema, listReports, getReport, } from './reports.js';
|
|
21
|
+
import { InterviewPersonaInputSchema, interviewPersona } from './interview.js';
|
|
22
|
+
function defineTool(def) {
|
|
23
|
+
return { ...def, jsonSchema: zodToInputSchema(def.zodSchema) };
|
|
24
|
+
}
|
|
25
|
+
export const TOOLS = [
|
|
26
|
+
defineTool({
|
|
27
|
+
name: 'clien_research',
|
|
28
|
+
description: 'Run a full product-research pass on an idea — the recommended default for validating ' +
|
|
29
|
+
'a new idea end to end. Returns a markdown report plus structured report_data ' +
|
|
30
|
+
'(hypotheses, competitors[], key findings). Typical wall time 15-25 minutes; progress ' +
|
|
31
|
+
'is streamed via notifications/progress. ' +
|
|
32
|
+
'This hero tool runs the whole curated pipeline in one call. PREFER it over hand-assembling ' +
|
|
33
|
+
'a full validation from the granular primitives (create_persona + interview_persona + get_report) — ' +
|
|
34
|
+
'those are for targeted, cheaper spot-checks, not a complete report; sequencing them yourself ' +
|
|
35
|
+
'gives lower-quality coverage. See docs/recipes/ for the supported compositions ' +
|
|
36
|
+
'(competitor spot-check, pivot viability, new-market persona validation). ' +
|
|
37
|
+
'First call opens a browser for one-time OAuth login; subsequent calls reuse the stored refresh token. ' +
|
|
38
|
+
'IMPORTANT: if this call fails mid-flight with "MCP error -32000: Connection closed", ' +
|
|
39
|
+
'do NOT call clien_research again — that spawns a new run and debits credits again for the same idea. ' +
|
|
40
|
+
'Instead call `clien_research_status` with the job_id from the prior call\'s `_meta.job_id` ' +
|
|
41
|
+
'(or from the `[clien-mcp] research started job_id=…` line in the MCP server\'s stderr log).',
|
|
42
|
+
zodSchema: ResearchInputSchema,
|
|
43
|
+
handler: (args, deps) => runResearch(args, deps),
|
|
44
|
+
}),
|
|
45
|
+
defineTool({
|
|
46
|
+
name: 'clien_research_status',
|
|
47
|
+
description: 'Fetch the current state of an existing `clien_research` job. Use this to ' +
|
|
48
|
+
'recover a run that disconnected with -32000 mid-flight (the job keeps running ' +
|
|
49
|
+
'on the backend; this tool re-attaches a read view) and to poll for completion. ' +
|
|
50
|
+
'If the job is complete, returns the full markdown report and structured report_data ' +
|
|
51
|
+
'in the same shape `clien_research` would have returned — no need to call research again. ' +
|
|
52
|
+
'If the job is still running, returns a brief status summary. If the job failed or ' +
|
|
53
|
+
'was cancelled, returns the failure reason. ' +
|
|
54
|
+
'RECOVERY WITHOUT A JOB_ID: if the -32000 drop left you with no job_id, call this tool ' +
|
|
55
|
+
'with NO arguments — it discovers your recent run(s). If exactly one is in flight it ' +
|
|
56
|
+
'resumes it automatically; if several come back, it returns a list of runs each with a ' +
|
|
57
|
+
'job_id and an idea snippet — re-call with the job_id whose idea matches the run you were ' +
|
|
58
|
+
'executing (never guess, so run A never receives run B\'s report). ' +
|
|
59
|
+
'Read-only: no credit cost, no side effects.',
|
|
60
|
+
zodSchema: StatusInputSchema,
|
|
61
|
+
handler: (args, deps) => getResearchStatus(args, deps),
|
|
62
|
+
}),
|
|
63
|
+
defineTool({
|
|
64
|
+
name: 'list_projects',
|
|
65
|
+
description: 'List the caller\'s Clien.ai projects (newest first, paginated). Returns each ' +
|
|
66
|
+
'project\'s name and id as text, plus the raw project array and pagination in ' +
|
|
67
|
+
'`_meta`. Use this to find an existing `project_id` to attach a `clien_research` ' +
|
|
68
|
+
'run to, or to check whether a project already exists before calling `create_project`. ' +
|
|
69
|
+
'Read-only: no credit cost, no side effects.',
|
|
70
|
+
zodSchema: ListProjectsInputSchema,
|
|
71
|
+
handler: (args, deps) => listProjects(args, deps),
|
|
72
|
+
}),
|
|
73
|
+
defineTool({
|
|
74
|
+
name: 'create_project',
|
|
75
|
+
description: 'Create a new Clien.ai project to group research runs, personas, and reports. ' +
|
|
76
|
+
'Returns the created project (including its `id`) in `_meta.project`; pass that id ' +
|
|
77
|
+
'as `project_id` on subsequent `clien_research` calls. Prefer calling `list_projects` ' +
|
|
78
|
+
'first to avoid duplicates. Note: `clien_research` can also auto-create a project via ' +
|
|
79
|
+
'its `project_name` argument — use this tool when you want to create the project up ' +
|
|
80
|
+
'front (e.g. with a description) or without starting a research run.',
|
|
81
|
+
zodSchema: CreateProjectInputSchema,
|
|
82
|
+
handler: (args, deps) => createProject(args, deps),
|
|
83
|
+
}),
|
|
84
|
+
defineTool({
|
|
85
|
+
name: 'create_persona',
|
|
86
|
+
description: 'Create a persona (a target user/customer profile) in a Clien.ai project from ' +
|
|
87
|
+
'fields you already know. Returns the created persona (including its `id`) in ' +
|
|
88
|
+
'`_meta.persona`. Requires a `projectId` — call `list_projects` first to find one, ' +
|
|
89
|
+
'or `create_project` to make one. Provide `name` and `role` at minimum; add ' +
|
|
90
|
+
'`company`, `description` (background/goals/pain points), `companySize`, `industry`, ' +
|
|
91
|
+
'and `location` when you have them. Set `ephemeral: true` for a scratch persona you ' +
|
|
92
|
+
'do NOT want counted in the project\'s aggregate insights (e.g. a quick one-off). ' +
|
|
93
|
+
'Compose with `interview_persona` to validate a NEW MARKET cheaply: create a persona ' +
|
|
94
|
+
'representing that market, then interview it (see docs/recipes/new-market-persona-validation.md). ' +
|
|
95
|
+
'For a FULL validation report on the new market rather than a targeted probe, prefer `clien_research`. ' +
|
|
96
|
+
'This is a free, no-credit create for explicit fields — AI generation/enrichment of ' +
|
|
97
|
+
'persona details is not yet available via MCP, so supply the fields yourself.',
|
|
98
|
+
zodSchema: CreatePersonaInputSchema,
|
|
99
|
+
handler: (args, deps) => createPersona(args, deps),
|
|
100
|
+
}),
|
|
101
|
+
defineTool({
|
|
102
|
+
name: 'list_reports',
|
|
103
|
+
description: 'List the caller\'s past `clien_research` runs (newest first, paginated by `limit`). ' +
|
|
104
|
+
'Returns each report\'s prompt, status, and `job_id` as text, plus the raw job array in ' +
|
|
105
|
+
'`_meta.jobs`. Optionally scope to one project with `project_id`. Use this to find a ' +
|
|
106
|
+
'past report to retrieve with `get_report` instead of re-running `clien_research` — ' +
|
|
107
|
+
're-running spawns a new job and debits credits again for the same idea. Pair with ' +
|
|
108
|
+
'`get_report` for a free competitor spot-check (read `report_data.competitors[]` from a ' +
|
|
109
|
+
'recent run; see docs/recipes/competitor-spot-check.md). ' +
|
|
110
|
+
'Read-only: no credit cost, no side effects.',
|
|
111
|
+
zodSchema: ListReportsInputSchema,
|
|
112
|
+
handler: (args, deps) => listReports(args, deps),
|
|
113
|
+
}),
|
|
114
|
+
defineTool({
|
|
115
|
+
name: 'get_report',
|
|
116
|
+
description: 'Retrieve the full report for a past `clien_research` job by its `job_id` (from a prior ' +
|
|
117
|
+
'run\'s `_meta.job_id` or a `list_reports` entry). Returns the markdown report as text ' +
|
|
118
|
+
'plus structured `report_data` (hypotheses, competitors[], key findings) in `_meta` — ' +
|
|
119
|
+
'the same shape `clien_research` returns. Use this to RETRIEVE past research instead of ' +
|
|
120
|
+
're-running `clien_research`, which would debit credits again for the same idea. ' +
|
|
121
|
+
'Backs the competitor spot-check (read `report_data.competitors[]`) and pivot-viability ' +
|
|
122
|
+
'(compare a pivot\'s report against the original) recipes in docs/recipes/. ' +
|
|
123
|
+
'If the job is still running or you are unsure of its state, use `clien_research_status`. ' +
|
|
124
|
+
'Read-only: no credit cost, no side effects.',
|
|
125
|
+
zodSchema: GetReportInputSchema,
|
|
126
|
+
handler: (args, deps) => getReport(args, deps),
|
|
127
|
+
}),
|
|
128
|
+
defineTool({
|
|
129
|
+
name: 'interview_persona',
|
|
130
|
+
description: 'Run a real, stateful multi-turn interview with a persona and get its full replies. ' +
|
|
131
|
+
'Drive it as a loop: `action:start` once (with projectId + personaId) opens the interview ' +
|
|
132
|
+
'and seeds the persona\'s greeting, returning the interview id in `_meta.interview.id`. ' +
|
|
133
|
+
'Then `action:turn` (interviewId + content) repeatedly — each call posts one question and ' +
|
|
134
|
+
'returns THAT persona\'s full reply as the result text; read it, then ask the next question. ' +
|
|
135
|
+
'`action:transcript` (interviewId) re-reads the whole conversation. `action:complete` closes ' +
|
|
136
|
+
'the interview when you\'ve learned enough (5-12 turns is typical); `action:reopen` resumes a ' +
|
|
137
|
+
'completed one. Interviews and their auto-summary/message-count behave exactly like the web app. ' +
|
|
138
|
+
'Set `ephemeral:true` on `start` for a scratch interview excluded from the project\'s aggregate ' +
|
|
139
|
+
'insights. Compose with `create_persona` to validate a new market or pressure-test a pivot ' +
|
|
140
|
+
'(see docs/recipes/); for a FULL validation report, prefer `clien_research` over stitching ' +
|
|
141
|
+
'interviews together yourself. Each `turn` generates a real AI reply (metered by your monthly ' +
|
|
142
|
+
'usage; per-turn credit pricing is not yet applied).',
|
|
143
|
+
zodSchema: InterviewPersonaInputSchema,
|
|
144
|
+
handler: (args, deps) => interviewPersona(args, deps),
|
|
145
|
+
}),
|
|
146
|
+
];
|
|
147
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/tools/registry.ts"],"names":[],"mappings":"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"}
|