@kolbo/mcp 1.61.0 → 1.62.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/package.json +1 -1
- package/src/tools/chat.js +9 -1
- package/src/tools/models.js +4 -0
- package/src/tools/projects.js +19 -6
- package/src/tools/visual_dna.js +17 -7
package/package.json
CHANGED
package/src/tools/chat.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { pollOrTimedOut, creditFields, projectIdField } = require('./_shared');
|
|
8
|
+
const { canonicalModelId } = require('../apps');
|
|
8
9
|
|
|
9
10
|
function registerChatTools(server, client) {
|
|
10
11
|
// ─── chat_send_message ─────────────────────────────────────
|
|
@@ -13,7 +14,7 @@ function registerChatTools(server, client) {
|
|
|
13
14
|
'Send a chat message to Kolbo AI. Starts a new conversation (omit session_id) or continues an existing one. Returns the assistant response when complete. Supports image/video/audio analysis via media_urls — pass public URLs and the model auto-routes to a vision-capable model (e.g. Gemini) when media is detected. Supports web search and deep think modes.',
|
|
14
15
|
{
|
|
15
16
|
message: z.string().describe('The user message to send'),
|
|
16
|
-
model: z.string().optional().describe('Model identifier
|
|
17
|
+
model: z.string().optional().describe('Model identifier from list_models type="text". Identifiers resolve leniently, so the DISPLAY NAME that list_models shows works too ("Grok 4.5" → its identifier). Do NOT hardcode an id you have not seen in list_models — the text catalog turns over fast. Prefer passing a SPECIFIC model — omitting falls back to Smart Select auto-routing, which we avoid unless the user explicitly asks for auto-pick. Exception: when media_urls contains video or audio, omitting is fine — routing goes to a Gemini vision model regardless of this field.'),
|
|
17
18
|
session_id: z.string().optional().describe('Existing chat session ID to continue. Omit to start a new conversation.'),
|
|
18
19
|
system_prompt: z.string().optional().describe('System prompt for the conversation. Only applied when creating a new session.'),
|
|
19
20
|
web_search: z.boolean().optional().describe('Enable web search for this message. Default: false'),
|
|
@@ -23,6 +24,13 @@ function registerChatTools(server, client) {
|
|
|
23
24
|
project_id: projectIdField
|
|
24
25
|
},
|
|
25
26
|
async ({ message, model, session_id, system_prompt, web_search, deep_think, enhance_prompt = false, media_urls, project_id }) => {
|
|
27
|
+
// Every generate_* tool resolves its model this way; chat was the one
|
|
28
|
+
// `model` arg that went straight to the API, which has no fuzzy matching.
|
|
29
|
+
// So the display names list_models hands back ("Claude Fable 5") came
|
|
30
|
+
// back as a bare `Model not found: Claude Fable 5 [MODEL_NOT_FOUND]` —
|
|
31
|
+
// discovery had no path to use.
|
|
32
|
+
model = await canonicalModelId(client, model, 'text'); // lenient id resolution ("Grok 4.5" → its identifier)
|
|
33
|
+
|
|
26
34
|
const gen = await client.post('/v1/chat', {
|
|
27
35
|
message,
|
|
28
36
|
model,
|
package/src/tools/models.js
CHANGED
|
@@ -67,6 +67,10 @@ function buildCatalogStructured(models, type, compact) {
|
|
|
67
67
|
if (g.models.length >= 6) continue; // curated cap — full list lives in the text payload
|
|
68
68
|
g.models.push({
|
|
69
69
|
name: m.name,
|
|
70
|
+
// The widget renders `name`; the AGENT reads the same rows (hosts hand it
|
|
71
|
+
// structuredContent). Without the identifier the default call was a dead
|
|
72
|
+
// end — it named six models and gave no way to pass any of them on.
|
|
73
|
+
identifier: m.identifier,
|
|
70
74
|
icon: resolveAvatarUrl(m.avatar),
|
|
71
75
|
description: String(m.smartSelect_StrengthsSummary || m.summary || m.description || '').slice(0, 90),
|
|
72
76
|
chips: modelChips(m),
|
package/src/tools/projects.js
CHANGED
|
@@ -12,21 +12,34 @@ function registerProjectTools(server, client, options = {}) {
|
|
|
12
12
|
// ─── list_projects ─────────────────────────────────────────
|
|
13
13
|
server.tool(
|
|
14
14
|
'list_projects',
|
|
15
|
-
'List the user\'s platform projects (owned + shared with edit/full/owner permission). Use this to resolve a project NAME the user mentioned ("put this in my Acme Campaign project") into the project ObjectId you pass back as `project_id` on generation / chat / upload / move tools. Whenever the user mentions a project by name OR location, you MUST call this first — those tools accept only ObjectIds, not names — and then pass the resolved `project_id` on EVERY subsequent call in the conversation (it is per-call, not sticky; omitting it drops work into the default bucket). Returns id, name, role, and
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
'List the user\'s platform projects (owned + shared with edit/full/owner permission). Use this to resolve a project NAME the user mentioned ("put this in my Acme Campaign project") into the project ObjectId you pass back as `project_id` on generation / chat / upload / move tools. Whenever the user mentions a project by name OR location, you MUST call this first — those tools accept only ObjectIds, not names — and then pass the resolved `project_id` on EVERY subsequent call in the conversation (it is per-call, not sticky; omitting it drops work into the default bucket). Returns id, name, role, is_default, and is_archived. The project flagged `is_default: true` is the auto-created "API Generations" bucket every SDK generation lands in when project_id is omitted. Accounts routinely have HUNDREDS of projects, so this is paginated: when you already know the name, pass `search` — it is far cheaper than listing everything. Default page size is 50; use `page` to walk the rest (`pagination.has_more` tells you when to stop). Archived projects are hidden unless you pass `include_archived: true`.',
|
|
16
|
+
{
|
|
17
|
+
search: z.string().optional().describe('Case-insensitive substring match on the project name. Use this whenever the user named a project — it turns a full listing into a one-item answer.'),
|
|
18
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1'),
|
|
19
|
+
limit: z.number().optional().describe('Results per page, max 200. Default: 50'),
|
|
20
|
+
include_archived: z.boolean().optional().describe('Also return archived projects. Default false — archived projects are hidden here exactly as they are in the web app.')
|
|
21
|
+
},
|
|
22
|
+
async ({ search, page, limit, include_archived }) => {
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
if (search) params.set('search', search);
|
|
25
|
+
if (page) params.set('page', String(page));
|
|
26
|
+
if (limit) params.set('limit', String(limit));
|
|
27
|
+
if (include_archived) params.set('include_archived', 'true');
|
|
28
|
+
const qs = params.toString();
|
|
29
|
+
const result = await client.get(`/v1/projects${qs ? '?' + qs : ''}`);
|
|
19
30
|
const projects = (result.projects || []).map(p => ({
|
|
20
31
|
id: p.id,
|
|
21
32
|
name: p.name,
|
|
22
33
|
role: p.role,
|
|
23
34
|
is_default: !!p.is_default,
|
|
35
|
+
is_archived: !!p.is_archived,
|
|
24
36
|
open_url: buildProjectUrl(p.id, { is_default: !!p.is_default })
|
|
25
37
|
}));
|
|
26
38
|
const text = JSON.stringify({
|
|
27
39
|
projects,
|
|
28
40
|
count: projects.length,
|
|
29
|
-
|
|
41
|
+
pagination: result.pagination || null,
|
|
42
|
+
_hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true. `open_url` opens that project\'s media in the web app (share it with the user). If `pagination.has_more` is true there are more projects — narrow with `search` rather than paging through everything.'
|
|
30
43
|
}, null, 2);
|
|
31
44
|
|
|
32
45
|
if (ui()) {
|
|
@@ -36,7 +49,7 @@ function registerProjectTools(server, client, options = {}) {
|
|
|
36
49
|
items: projects.map(p => ({
|
|
37
50
|
id: p.id,
|
|
38
51
|
title: p.name,
|
|
39
|
-
subtitle: p.role + (p.is_default ? ' · default' : ''),
|
|
52
|
+
subtitle: p.role + (p.is_default ? ' · default' : '') + (p.is_archived ? ' · archived' : ''),
|
|
40
53
|
open_url: p.open_url,
|
|
41
54
|
use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
|
|
42
55
|
})),
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -87,15 +87,19 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
87
87
|
+ 'because dumping them buries the user\'s own handful. Only pass scope="global" (optionally with '
|
|
88
88
|
+ '`collection` and `search`) when the user explicitly wants to BROWSE the preset cast — e.g. "find me a '
|
|
89
89
|
+ 'character", "show me street style models", "I need a location DNA" — and they have not named one of '
|
|
90
|
-
+ 'their own. Use scope="all" only if the user genuinely wants both at once.'
|
|
90
|
+
+ 'their own. Use scope="all" only if the user genuinely wants both at once. '
|
|
91
|
+
+ 'The response reports `total` and `_truncated`; when there are more matches than one '
|
|
92
|
+
+ 'page holds, raise `limit` or walk `page` — do not tell the user the extras do not exist.',
|
|
91
93
|
{
|
|
92
94
|
scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Default: "personal" — the user\'s own DNAs (plus a shared project\'s when project_id is set). "global" = the ~1000 system cast/preset DNAs, for browsing when the user needs a character and has none of their own. "organization" = org-shared. "all" = everything, rarely wanted.'),
|
|
93
|
-
search: z.string().optional().describe('Search by name, tags, or description (case-insensitive)'),
|
|
95
|
+
search: z.string().optional().describe('Search by name, tags, or description (case-insensitive). Matches at WORD STARTS, so "man" finds "Man"/"Manager" but not "woman" or "romantic". Name matches are ranked first.'),
|
|
94
96
|
collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street'),
|
|
95
97
|
tags: z.string().optional().describe('Comma-separated tags to filter by (OR logic)'),
|
|
98
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1. Needed to reach the global cast beyond the first page.'),
|
|
99
|
+
limit: z.number().optional().describe('Results per page, max 100. Default: 50'),
|
|
96
100
|
project_id: projectScopeReadField
|
|
97
101
|
},
|
|
98
|
-
async ({ scope, search, collection, tags, project_id } = {}) => {
|
|
102
|
+
async ({ scope, search, collection, tags, page, limit, project_id } = {}) => {
|
|
99
103
|
const params = new URLSearchParams();
|
|
100
104
|
// Default to the user's OWN DNAs. The API defaults to "all", which pulls
|
|
101
105
|
// in ~1000 global cast presets and buries the handful the user actually
|
|
@@ -106,17 +110,23 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
106
110
|
if (search) params.set('search', search);
|
|
107
111
|
if (collection) params.set('collection', collection);
|
|
108
112
|
if (tags) params.set('tags', tags);
|
|
113
|
+
// Always paged. Without these the ~1000-item global cast came back whole and
|
|
114
|
+
// was silently cut to the display cap, so nothing past the first screen was
|
|
115
|
+
// reachable through this tool at all.
|
|
116
|
+
params.set('page', String(page && page > 0 ? Math.floor(page) : 1));
|
|
117
|
+
params.set('limit', String(limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50));
|
|
109
118
|
if (project_id) params.set('project_id', project_id);
|
|
110
119
|
const qs = params.toString();
|
|
111
120
|
const result = await client.get(`/v1/visual-dna${qs ? '?' + qs : ''}`);
|
|
112
121
|
const dnas = result.visual_dnas || [];
|
|
122
|
+
const total = result.total != null ? result.total : (result.count || dnas.length);
|
|
113
123
|
// Full profiles measured 74,310 chars — the embedded analysis/description
|
|
114
124
|
// blobs are large and the model only needs enough to pick an id.
|
|
115
125
|
const text = compactList(dnas, {
|
|
116
126
|
fields: ['id', 'name', 'type', 'folder_id', 'tags', 'thumbnail'],
|
|
117
127
|
cap: 60,
|
|
118
|
-
total
|
|
119
|
-
note: 'Narrow with `search`, `tags`, or `collection
|
|
128
|
+
total,
|
|
129
|
+
note: 'Narrow with `search`, `tags`, or `collection`, or pass `page`/`limit` for the rest; get_visual_dna returns one in full.',
|
|
120
130
|
});
|
|
121
131
|
|
|
122
132
|
if (ui()) {
|
|
@@ -131,8 +141,8 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
131
141
|
media_type: 'image',
|
|
132
142
|
use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
|
|
133
143
|
})),
|
|
134
|
-
total
|
|
135
|
-
has_more: dnas.length > 24
|
|
144
|
+
total,
|
|
145
|
+
has_more: result.has_more || dnas.length > 24
|
|
136
146
|
});
|
|
137
147
|
}
|
|
138
148
|
|