@harkenai/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 +98 -0
- package/package.json +41 -0
- package/src/index.js +213 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @harkenai/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for HarkenAI. Gives Claude Desktop, Cursor,
|
|
4
|
+
Claude Code, or any MCP-aware host direct access to your HarkenAI
|
|
5
|
+
sessions, transcripts, and contacts — no glue code, no scraping.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Issue an API key from **Settings → API Keys** in the HarkenAI web app.
|
|
10
|
+
Copy the secret (`hkn_…`) — you'll only see it once.
|
|
11
|
+
|
|
12
|
+
You don't need to install the package globally — most MCP hosts will
|
|
13
|
+
spawn it via `npx`. The example configs below all use that pattern.
|
|
14
|
+
|
|
15
|
+
## Wire it up
|
|
16
|
+
|
|
17
|
+
### Claude Desktop
|
|
18
|
+
|
|
19
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
20
|
+
(macOS) or `%APPDATA%/Claude/claude_desktop_config.json` (Windows):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"harkenai": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "@harkenai/mcp-server"],
|
|
28
|
+
"env": {
|
|
29
|
+
"HARKENAI_API_KEY": "hkn_..."
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Restart Claude Desktop. The HarkenAI tools appear in the tool tray.
|
|
37
|
+
|
|
38
|
+
### Cursor
|
|
39
|
+
|
|
40
|
+
Add to `~/.cursor/mcp.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"harkenai": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "@harkenai/mcp-server"],
|
|
48
|
+
"env": {
|
|
49
|
+
"HARKENAI_API_KEY": "hkn_..."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Claude Code
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add harkenai \
|
|
60
|
+
--env HARKENAI_API_KEY=hkn_... \
|
|
61
|
+
-- npx -y @harkenai/mcp-server
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tools
|
|
65
|
+
|
|
66
|
+
| Tool | Purpose |
|
|
67
|
+
|-----------------------------|-------------------------------------------------------------------------|
|
|
68
|
+
| `harken.search_sessions` | Paginated session list. Filters: `q`, `since`, `until`, `tag`, `personName`. |
|
|
69
|
+
| `harken.get_session` | Full payload for one session (summary, insights, action items, etc.). |
|
|
70
|
+
| `harken.get_transcript` | Transcript text or JSON for one session. `format=text` is ideal for LLMs. |
|
|
71
|
+
| `harken.list_recent` | Shorthand: sessions from the last N days. |
|
|
72
|
+
| `harken.search_contacts` | Search contacts by name / company. |
|
|
73
|
+
|
|
74
|
+
## Environment
|
|
75
|
+
|
|
76
|
+
| Variable | Required | Default | Notes |
|
|
77
|
+
|----------------------|----------|----------------------------------------|------------------------------------|
|
|
78
|
+
| `HARKENAI_API_KEY` | yes | — | Issue from Settings → API Keys. |
|
|
79
|
+
| `HARKENAI_API_URL` | no | `https://harkenai-api.onrender.com` | Override for staging / self-hosted. |
|
|
80
|
+
|
|
81
|
+
## Common prompts
|
|
82
|
+
|
|
83
|
+
Once the server is registered, try asking the host:
|
|
84
|
+
|
|
85
|
+
> "Summarize my conversations from last week using HarkenAI."
|
|
86
|
+
>
|
|
87
|
+
> "What did Alice and I discuss about pricing? Pull the transcript and
|
|
88
|
+
> quote the relevant lines."
|
|
89
|
+
>
|
|
90
|
+
> "List every session where 'compliance' was mentioned and give me the
|
|
91
|
+
> action items."
|
|
92
|
+
|
|
93
|
+
Each of those drives `harken.list_recent` / `harken.search_sessions`
|
|
94
|
+
plus `harken.get_transcript` under the hood.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harkenai/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for HarkenAI — gives Claude / Cursor / Claude Desktop direct access to your sessions, transcripts, and contacts via the HarkenAI Developer API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"harkenai-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"harkenai",
|
|
23
|
+
"claude"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://harkenai.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/pbrosen/harkenai.git",
|
|
30
|
+
"directory": "mcp-server"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/pbrosen/harkenai/issues"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HarkenAI MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Speaks the Model Context Protocol over stdio. Agents using Claude
|
|
6
|
+
* Desktop / Cursor / Claude Code can register this server with one
|
|
7
|
+
* config line and then call HarkenAI tools from any chat — no glue
|
|
8
|
+
* code required.
|
|
9
|
+
*
|
|
10
|
+
* Auth: HARKENAI_API_KEY env var (issued from Settings → API Keys in
|
|
11
|
+
* the HarkenAI web app). Optional HARKENAI_API_URL overrides the base
|
|
12
|
+
* URL — defaults to production.
|
|
13
|
+
*
|
|
14
|
+
* Tools exposed:
|
|
15
|
+
* harken.search_sessions filter + paginate sessions
|
|
16
|
+
* harken.get_session full payload for one session
|
|
17
|
+
* harken.get_transcript transcript text / JSON for one session
|
|
18
|
+
* harken.list_recent shorthand for "last N days"
|
|
19
|
+
* harken.search_contacts contacts by name / company
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import {
|
|
25
|
+
CallToolRequestSchema,
|
|
26
|
+
ListToolsRequestSchema,
|
|
27
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
28
|
+
|
|
29
|
+
const API_URL = (process.env.HARKENAI_API_URL || 'https://harkenai-api.onrender.com').replace(/\/$/, '');
|
|
30
|
+
const RAW_API_KEY = process.env.HARKENAI_API_KEY || '';
|
|
31
|
+
// Trim defensively. The #1 cause of 401s here is a stray trailing newline
|
|
32
|
+
// or leading whitespace in the host's config — Claude Desktop / Cursor
|
|
33
|
+
// JSON-encode the env var literally, so any whitespace the user pasted
|
|
34
|
+
// rides along into the Authorization header and the server rejects it.
|
|
35
|
+
const API_KEY = RAW_API_KEY.trim();
|
|
36
|
+
|
|
37
|
+
if (!API_KEY) {
|
|
38
|
+
console.error('[harkenai-mcp] HARKENAI_API_KEY is not set. Issue a key in Settings → API Keys.');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
if (RAW_API_KEY !== API_KEY) {
|
|
42
|
+
console.error('[harkenai-mcp] WARNING: HARKENAI_API_KEY had surrounding whitespace; trimmed before use.');
|
|
43
|
+
}
|
|
44
|
+
if (!API_KEY.startsWith('hkn_')) {
|
|
45
|
+
console.error(`[harkenai-mcp] WARNING: HARKENAI_API_KEY does not start with "hkn_". Got prefix: ${API_KEY.slice(0, 6)}…`);
|
|
46
|
+
}
|
|
47
|
+
console.error(`[harkenai-mcp] starting against ${API_URL} with key ${API_KEY.slice(0, 12)}… (${API_KEY.length} chars)`);
|
|
48
|
+
|
|
49
|
+
async function call(endpoint, init = {}) {
|
|
50
|
+
const url = `${API_URL}${endpoint}`;
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
...init,
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
...(init.headers || {}),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const text = await res.text().catch(() => '');
|
|
61
|
+
// Log the failing URL + status to stderr so users can see it in
|
|
62
|
+
// ~/Library/Logs/Claude/mcp-server-harkenai.log (or equivalent) when
|
|
63
|
+
// a tool call mysteriously fails.
|
|
64
|
+
console.error(`[harkenai-mcp] ${init.method || 'GET'} ${endpoint} -> ${res.status} ${res.statusText}`);
|
|
65
|
+
throw new Error(`HarkenAI API ${res.status}: ${text || res.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
// Streamed responses (e.g. /transcripts) come back as text; the
|
|
68
|
+
// helpers below detect content-type and parse appropriately.
|
|
69
|
+
const ct = res.headers.get('content-type') || '';
|
|
70
|
+
if (ct.includes('application/json')) return res.json();
|
|
71
|
+
if (ct.includes('application/x-ndjson')) return res.text();
|
|
72
|
+
return res.text();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TOOLS = [
|
|
76
|
+
{
|
|
77
|
+
name: 'harken.search_sessions',
|
|
78
|
+
description:
|
|
79
|
+
'Search HarkenAI sessions. Returns lightweight summaries (id, name, date, duration, peopleCount, tags, summary). Use harken.get_transcript next when you need the full text.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
q: { type: 'string', description: 'Full-text search across session content' },
|
|
84
|
+
since: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'ISO date — only sessions updated on or after this',
|
|
87
|
+
},
|
|
88
|
+
until: { type: 'string', description: 'ISO date — only sessions updated on or before this' },
|
|
89
|
+
tag: { type: 'string', description: 'Filter by tag (comma-separated for OR)' },
|
|
90
|
+
personName: { type: 'string', description: 'Filter sessions that include this person' },
|
|
91
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 25 },
|
|
92
|
+
cursor: { type: 'string', description: 'updatedAt cursor from a previous page' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'harken.get_session',
|
|
98
|
+
description: 'Fetch the full payload (summary, insights, action items, conversations, contacts) for one session by id.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: { id: { type: 'string' } },
|
|
102
|
+
required: ['id'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'harken.get_transcript',
|
|
107
|
+
description:
|
|
108
|
+
'Fetch the transcript for one session. Defaults to plain text (speaker-prefixed) which is best for LLM consumption. Pass format=json for the structured payload including segments, speakerAssignments, and a `participants` array that joins each speaker to their Contact (name, role, company) — saving you a second call when you need titles or affiliations. Markdown output (format=markdown) starts with a "## Participants" block.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
id: { type: 'string' },
|
|
113
|
+
format: { type: 'string', enum: ['text', 'json', 'markdown'], default: 'text' },
|
|
114
|
+
withParticipants: { type: 'boolean', default: false, description: 'Only honored for format=text — when true, prepends a participants block to the plain-text body.' },
|
|
115
|
+
},
|
|
116
|
+
required: ['id'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'harken.list_recent',
|
|
121
|
+
description: 'List sessions from the last N days. Shorthand for search_sessions with a relative since= date.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
days: { type: 'integer', minimum: 1, maximum: 365, default: 7 },
|
|
126
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 25 },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'harken.search_contacts',
|
|
132
|
+
description: 'Search contacts known to this HarkenAI user.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
q: { type: 'string' },
|
|
137
|
+
company: { type: 'string' },
|
|
138
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 25 },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function qs(params) {
|
|
145
|
+
const search = new URLSearchParams();
|
|
146
|
+
for (const [k, v] of Object.entries(params || {})) {
|
|
147
|
+
if (v === undefined || v === null || v === '') continue;
|
|
148
|
+
search.set(k, String(v));
|
|
149
|
+
}
|
|
150
|
+
const s = search.toString();
|
|
151
|
+
return s ? `?${s}` : '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function runTool(name, args) {
|
|
155
|
+
switch (name) {
|
|
156
|
+
case 'harken.search_sessions': {
|
|
157
|
+
const data = await call(`/api/v1/sessions${qs(args)}`);
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
case 'harken.get_session': {
|
|
161
|
+
if (!args?.id) throw new Error('id is required');
|
|
162
|
+
return call(`/api/v1/sessions/${encodeURIComponent(args.id)}`);
|
|
163
|
+
}
|
|
164
|
+
case 'harken.get_transcript': {
|
|
165
|
+
if (!args?.id) throw new Error('id is required');
|
|
166
|
+
const fmt = args.format === 'json' ? 'json'
|
|
167
|
+
: args.format === 'markdown' ? 'md'
|
|
168
|
+
: 'txt';
|
|
169
|
+
const params = { format: fmt };
|
|
170
|
+
if (fmt === 'txt' && args.withParticipants) params.withParticipants = 'true';
|
|
171
|
+
const path = `/api/v1/sessions/${encodeURIComponent(args.id)}/transcript${qs(params)}`;
|
|
172
|
+
const result = await call(path);
|
|
173
|
+
return typeof result === 'string' ? { text: result, format: fmt } : result;
|
|
174
|
+
}
|
|
175
|
+
case 'harken.list_recent': {
|
|
176
|
+
const days = Math.min(Math.max(Number(args?.days) || 7, 1), 365);
|
|
177
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
178
|
+
return call(`/api/v1/sessions${qs({ since, limit: args?.limit || 25 })}`);
|
|
179
|
+
}
|
|
180
|
+
case 'harken.search_contacts': {
|
|
181
|
+
return call(`/api/v1/contacts${qs(args)}`);
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const server = new Server(
|
|
189
|
+
{ name: 'harkenai-mcp', version: '0.1.0' },
|
|
190
|
+
{ capabilities: { tools: {} } },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
194
|
+
|
|
195
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
196
|
+
const { name, arguments: args } = req.params;
|
|
197
|
+
try {
|
|
198
|
+
const result = await runTool(name, args || {});
|
|
199
|
+
const payload = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: payload }],
|
|
202
|
+
};
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
isError: true,
|
|
206
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const transport = new StdioServerTransport();
|
|
212
|
+
await server.connect(transport);
|
|
213
|
+
console.error('[harkenai-mcp] connected. Waiting for tool calls…');
|