@velaro/mcp-server 0.1.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/README.md +73 -0
- package/package.json +32 -0
- package/server.js +287 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @velaro/mcp-server
|
|
2
|
+
|
|
3
|
+
Connect Claude, Claude Code, and other AI agents directly to your Velaro account via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @velaro/mcp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Set your MCP key (create one in Velaro Settings > API > MCP Keys):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
VELARO_MCP_KEY=vel_live_... npx @velaro/mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Claude Code Setup
|
|
18
|
+
|
|
19
|
+
Add to `~/.claude/settings.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"velaro": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["@velaro/mcp-server"],
|
|
27
|
+
"env": {
|
|
28
|
+
"VELARO_MCP_KEY": "vel_live_..."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Claude Desktop Setup
|
|
36
|
+
|
|
37
|
+
Add to `~/.claude/claude_desktop_config.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"velaro": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["@velaro/mcp-server"],
|
|
45
|
+
"env": {
|
|
46
|
+
"VELARO_MCP_KEY": "vel_live_..."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Available Tools
|
|
54
|
+
|
|
55
|
+
| Tool | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `kb_list_topics` | List all KB topics |
|
|
58
|
+
| `kb_search_articles` | Search articles by term, topic, or publish status |
|
|
59
|
+
| `kb_get_article` | Get full article content by ID |
|
|
60
|
+
| `kb_create_article` | Create a new article |
|
|
61
|
+
| `kb_update_article` | Update an existing article |
|
|
62
|
+
| `kb_delete_article` | Delete an article |
|
|
63
|
+
|
|
64
|
+
## Auth
|
|
65
|
+
|
|
66
|
+
- `VELARO_MCP_KEY=vel_live_...` — service key (create in Settings > API > MCP Keys)
|
|
67
|
+
- `VELARO_JWT=<token>` — short-lived JWT from `velaro login` (dev use only)
|
|
68
|
+
|
|
69
|
+
## Staging / Custom API
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
VELARO_ADMIN_API=https://velaro-admin-staging.azurewebsites.net VELARO_MCP_KEY=vel_live_... npx @velaro/mcp-server
|
|
73
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velaro/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro KB and account.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"velaro-mcp": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node server.js"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/velaro/velaro-admin"
|
|
25
|
+
},
|
|
26
|
+
"keywords": ["velaro", "mcp", "knowledge-base", "claude", "ai", "model-context-protocol"],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"server.js",
|
|
30
|
+
"README.md"
|
|
31
|
+
]
|
|
32
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Velaro MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes KB article management as Claude tools via the Model Context Protocol.
|
|
6
|
+
*
|
|
7
|
+
* Auth (pick one):
|
|
8
|
+
* VELARO_MCP_KEY=vel_live_... service key (create with: velaro mcp-key create --label "Claude Code")
|
|
9
|
+
* VELARO_JWT=<token> short-lived Velaro JWT (from velaro login, for dev use)
|
|
10
|
+
*
|
|
11
|
+
* API base:
|
|
12
|
+
* VELARO_ADMIN_API=https://help.velaro.com (default, production)
|
|
13
|
+
* VELARO_ADMIN_API=https://velaro-admin-staging.azurewebsites.net (staging)
|
|
14
|
+
*
|
|
15
|
+
* Claude Desktop config (~/.claude/claude_desktop_config.json):
|
|
16
|
+
* {
|
|
17
|
+
* "mcpServers": {
|
|
18
|
+
* "velaro": {
|
|
19
|
+
* "command": "node",
|
|
20
|
+
* "args": ["/path/to/velaro-admin/mcp/server.js"],
|
|
21
|
+
* "env": { "VELARO_MCP_KEY": "vel_live_..." }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Claude Code config (settings.json → mcpServers):
|
|
27
|
+
* "velaro": {
|
|
28
|
+
* "command": "node",
|
|
29
|
+
* "args": ["/path/to/velaro-admin/mcp/server.js"],
|
|
30
|
+
* "env": { "VELARO_MCP_KEY": "vel_live_..." }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* Hosted HTTP endpoint (no Node required -- for customers and production):
|
|
34
|
+
* "velaro": {
|
|
35
|
+
* "url": "https://velaro-admin-staging.azurewebsites.net/mcp",
|
|
36
|
+
* "headers": { "Authorization": "Bearer vel_live_..." }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* HTTP mode (set VELARO_MCP_MODE=http to run as an HTTP/SSE server instead of stdio):
|
|
40
|
+
* VELARO_MCP_MODE=http PORT=3000 node server.js
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
44
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
45
|
+
import {
|
|
46
|
+
CallToolRequestSchema,
|
|
47
|
+
ListToolsRequestSchema,
|
|
48
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
49
|
+
|
|
50
|
+
const API_BASE = process.env.VELARO_ADMIN_API || 'https://help.velaro.com';
|
|
51
|
+
const MCP_KEY = process.env.VELARO_MCP_KEY;
|
|
52
|
+
const JWT = process.env.VELARO_JWT;
|
|
53
|
+
|
|
54
|
+
if (!MCP_KEY && !JWT) {
|
|
55
|
+
process.stderr.write('ERROR: Set VELARO_MCP_KEY=vel_live_... or VELARO_JWT=<token>\n');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function authHeader() {
|
|
60
|
+
return MCP_KEY ? `Bearer ${MCP_KEY}` : `Bearer ${JWT}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function api(method, path, body) {
|
|
64
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
65
|
+
method,
|
|
66
|
+
headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
|
|
67
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const text = await res.text().catch(() => '');
|
|
71
|
+
throw new Error(`Velaro API ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
|
72
|
+
}
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
return text ? JSON.parse(text) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const TOOLS = [
|
|
80
|
+
{
|
|
81
|
+
name: 'kb_list_topics',
|
|
82
|
+
description: 'List all KB topics. Call this first to find the topicId needed for creating articles.',
|
|
83
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'kb_search_articles',
|
|
87
|
+
description: 'Search KB articles. Returns id, title, slug, topicName, hitCount, isPublished, and public URL.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
searchTerm: { type: 'string', description: 'Full-text search term' },
|
|
92
|
+
topicId: { type: 'number', description: 'Filter by topic ID' },
|
|
93
|
+
publishStatus: { type: 'string', enum: ['Published', 'Unpublished'], description: 'Filter by publish status' },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'kb_get_article',
|
|
99
|
+
description: 'Get full content of a KB article by ID.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: { id: { type: 'number', description: 'Article ID' } },
|
|
103
|
+
required: ['id'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'kb_create_article',
|
|
108
|
+
description: 'Create a new KB article. Use kb_list_topics first to find the topicId.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
title: { type: 'string', description: 'Article title' },
|
|
113
|
+
slug: { type: 'string', description: 'URL slug (lowercase, hyphens only)' },
|
|
114
|
+
topicId: { type: 'number', description: 'Topic ID from kb_list_topics' },
|
|
115
|
+
content: { type: 'string', description: 'Article body as HTML' },
|
|
116
|
+
description: { type: 'string', description: 'Short description (shown in search results)' },
|
|
117
|
+
searchTags: { type: 'string', description: 'Space-separated tags for search' },
|
|
118
|
+
isPublished: { type: 'boolean', description: 'Publish immediately (default: false)' },
|
|
119
|
+
botSummary: { type: 'string', description: 'Concise bot-readable summary (max 800 chars)' },
|
|
120
|
+
},
|
|
121
|
+
required: ['title', 'slug', 'topicId', 'content'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'kb_update_article',
|
|
126
|
+
description: 'Update an existing KB article. Fetch with kb_get_article first, then pass all fields.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
id: { type: 'number', description: 'Article ID to update' },
|
|
131
|
+
title: { type: 'string' },
|
|
132
|
+
slug: { type: 'string' },
|
|
133
|
+
topicId: { type: 'number' },
|
|
134
|
+
content: { type: 'string', description: 'Full article body as HTML' },
|
|
135
|
+
description: { type: 'string' },
|
|
136
|
+
searchTags: { type: 'string' },
|
|
137
|
+
isPublished: { type: 'boolean' },
|
|
138
|
+
botSummary: { type: 'string' },
|
|
139
|
+
},
|
|
140
|
+
required: ['id', 'title', 'slug', 'topicId', 'content'],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'kb_delete_article',
|
|
145
|
+
description: 'Delete a KB article. Irreversible — confirm with the user before calling.',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: { id: { type: 'number', description: 'Article ID to delete' } },
|
|
149
|
+
required: ['id'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function handleTool(name, args) {
|
|
157
|
+
switch (name) {
|
|
158
|
+
case 'kb_list_topics': {
|
|
159
|
+
const data = await api('GET', '/api/kb/topics');
|
|
160
|
+
const topics = data?.topics ?? [];
|
|
161
|
+
return topics.map(t => `[${t.id}] ${t.name} (${t.articleCount} articles)`).join('\n')
|
|
162
|
+
|| 'No topics found.';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'kb_search_articles': {
|
|
166
|
+
const data = await api('POST', '/api/kb/articles/search', {
|
|
167
|
+
searchTerm: args.searchTerm,
|
|
168
|
+
topicId: args.topicId,
|
|
169
|
+
publishStatus: args.publishStatus,
|
|
170
|
+
});
|
|
171
|
+
const articles = data?.articles ?? [];
|
|
172
|
+
if (!articles.length) return 'No articles found.';
|
|
173
|
+
return articles.map(a =>
|
|
174
|
+
`[${a.id}] ${a.isPublished ? 'published' : 'draft'} | "${a.title}" | ${a.hitCount ?? 0} views | ${a.url ?? 'not public'}`
|
|
175
|
+
).join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'kb_get_article': {
|
|
179
|
+
const a = await api('GET', `/api/kb/articles?id=${args.id}`);
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
id: a.id,
|
|
182
|
+
title: a.title,
|
|
183
|
+
slug: a.slug,
|
|
184
|
+
topicId: a.topicId,
|
|
185
|
+
description: a.description,
|
|
186
|
+
content: a.content,
|
|
187
|
+
searchTags: a.searchTags,
|
|
188
|
+
isPublished: a.isPublished,
|
|
189
|
+
botSummary: a.botSummary,
|
|
190
|
+
url: a.url,
|
|
191
|
+
}, null, 2);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'kb_create_article': {
|
|
195
|
+
await api('POST', '/api/kb/articles', {
|
|
196
|
+
title: args.title,
|
|
197
|
+
slug: args.slug,
|
|
198
|
+
topicId: args.topicId,
|
|
199
|
+
content: args.content,
|
|
200
|
+
description: args.description || '',
|
|
201
|
+
searchTags: args.searchTags || '',
|
|
202
|
+
visibility: 'Public',
|
|
203
|
+
isPublished: args.isPublished ?? false,
|
|
204
|
+
displayPriority: 0,
|
|
205
|
+
enableBotResponse: !!args.botSummary,
|
|
206
|
+
botSummary: args.botSummary || '',
|
|
207
|
+
isFeatured: false,
|
|
208
|
+
});
|
|
209
|
+
const result = await api('POST', '/api/kb/articles/search', { searchTerm: args.slug, topicId: args.topicId });
|
|
210
|
+
const created = result?.articles?.find(a => a.slug === args.slug);
|
|
211
|
+
const id = created?.id;
|
|
212
|
+
return id
|
|
213
|
+
? `Created article [${id}] "${args.title}". URL: https://help.velaro.com/kb/article/${id}/${args.slug}`
|
|
214
|
+
: `Created "${args.title}" — search by slug "${args.slug}" to find the ID.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'kb_update_article': {
|
|
218
|
+
await api('PUT', `/api/kb/articles?id=${args.id}`, {
|
|
219
|
+
title: args.title,
|
|
220
|
+
slug: args.slug,
|
|
221
|
+
topicId: args.topicId,
|
|
222
|
+
content: args.content,
|
|
223
|
+
description: args.description || '',
|
|
224
|
+
searchTags: args.searchTags || '',
|
|
225
|
+
visibility: 'Public',
|
|
226
|
+
isPublished: args.isPublished ?? false,
|
|
227
|
+
displayPriority: 0,
|
|
228
|
+
enableBotResponse: !!args.botSummary,
|
|
229
|
+
botSummary: args.botSummary || '',
|
|
230
|
+
isFeatured: false,
|
|
231
|
+
});
|
|
232
|
+
return `Updated article [${args.id}] "${args.title}".`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'kb_delete_article': {
|
|
236
|
+
await api('DELETE', `/api/kb/articles?id=${args.id}`);
|
|
237
|
+
return `Article ${args.id} deleted.`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
default:
|
|
241
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
const server = new Server(
|
|
248
|
+
{ name: 'velaro', version: '0.1.0' },
|
|
249
|
+
{ capabilities: { tools: {} } }
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
253
|
+
|
|
254
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
255
|
+
const { name, arguments: args } = request.params;
|
|
256
|
+
try {
|
|
257
|
+
const result = await handleTool(name, args ?? {});
|
|
258
|
+
return { content: [{ type: 'text', text: String(result) }] };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const mode = process.env.VELARO_MCP_MODE || 'stdio';
|
|
265
|
+
|
|
266
|
+
if (mode === 'http') {
|
|
267
|
+
const { SSEServerTransport } = await import('@modelcontextprotocol/sdk/server/sse.js');
|
|
268
|
+
const http = await import('http');
|
|
269
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
270
|
+
|
|
271
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
272
|
+
if (req.method === 'GET' && req.url === '/mcp') {
|
|
273
|
+
const transport = new SSEServerTransport('/mcp', res);
|
|
274
|
+
await server.connect(transport);
|
|
275
|
+
} else if (req.method === 'POST' && req.url === '/mcp') {
|
|
276
|
+
// handled by SSEServerTransport automatically
|
|
277
|
+
} else {
|
|
278
|
+
res.writeHead(404).end();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
httpServer.listen(port, () =>
|
|
283
|
+
process.stderr.write(`Velaro MCP HTTP server listening on port ${port}\n`));
|
|
284
|
+
} else {
|
|
285
|
+
const transport = new StdioServerTransport();
|
|
286
|
+
await server.connect(transport);
|
|
287
|
+
}
|