@soundbi/sound-connect 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.
Files changed (42) hide show
  1. package/README.md +111 -0
  2. package/dist/__tests__/ingest.test.d.ts +18 -0
  3. package/dist/__tests__/ingest.test.d.ts.map +1 -0
  4. package/dist/__tests__/ingest.test.js +639 -0
  5. package/dist/__tests__/ingest.test.js.map +1 -0
  6. package/dist/__tests__/isolation.test.d.ts +12 -0
  7. package/dist/__tests__/isolation.test.d.ts.map +1 -0
  8. package/dist/__tests__/isolation.test.js +149 -0
  9. package/dist/__tests__/isolation.test.js.map +1 -0
  10. package/dist/__tests__/retry-queue.test.d.ts +11 -0
  11. package/dist/__tests__/retry-queue.test.d.ts.map +1 -0
  12. package/dist/__tests__/retry-queue.test.js +458 -0
  13. package/dist/__tests__/retry-queue.test.js.map +1 -0
  14. package/dist/auth.d.ts +80 -0
  15. package/dist/auth.d.ts.map +1 -0
  16. package/dist/auth.js +211 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/config.d.ts +35 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +66 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +100 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ingest.d.ts +253 -0
  27. package/dist/ingest.d.ts.map +1 -0
  28. package/dist/ingest.js +573 -0
  29. package/dist/ingest.js.map +1 -0
  30. package/dist/proxy.d.ts +79 -0
  31. package/dist/proxy.d.ts.map +1 -0
  32. package/dist/proxy.js +217 -0
  33. package/dist/proxy.js.map +1 -0
  34. package/dist/retry-queue.d.ts +236 -0
  35. package/dist/retry-queue.d.ts.map +1 -0
  36. package/dist/retry-queue.js +461 -0
  37. package/dist/retry-queue.js.map +1 -0
  38. package/dist/tools.d.ts +75 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +368 -0
  41. package/dist/tools.js.map +1 -0
  42. package/package.json +36 -0
package/dist/tools.js ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Tool registration for the Sound Connect bridge
3
+ * (STORY-006, STORY-009, STORY-010, STORY-011, STORY-012, STORY-013).
4
+ *
5
+ * STORY-010 / ADR-011: When the peer has not authenticated (no valid token at MCP
6
+ * startup), every tool call returns a clear MCP error directing the user to run
7
+ * `npx @soundbi/sound-connect login` in a terminal. The server never blocks on an
8
+ * interactive device-code prompt — that would cause a silent hang because Claude
9
+ * spawns the stdio server with stdout used for MCP framing (not displayed to the user).
10
+ *
11
+ * STORY-009 / ADR-005: One-client-per-instance isolation guard.
12
+ * - The bound clientSlug is fixed at process start. There is NO tool or command
13
+ * to switch clients in-session — any attempt to target a different slug is blocked.
14
+ * - assertResponseSlug() validates every backend response matches the bound slug so
15
+ * a misdirected backend response can never leak into this session.
16
+ *
17
+ * STORY-011 / ADR-006: ingest_file tool reads a local markdown file or transcript,
18
+ * normalizes, computes sha256, attaches provenance, and POSTs to /ingest/:slug.
19
+ *
20
+ * STORY-012 / ADR-006: ingest_folder tool bulk-ingests a directory of .md and
21
+ * transcript (.txt/.vtt/.srt) files, returning a per-file result table.
22
+ *
23
+ * STORY-013 / ADR-011: ingest_file and ingest_folder use queueOnFailure=true so
24
+ * transient backend failures (network / 5xx) queue chunks locally for retry.
25
+ * ingest_status tool reports pending/dead queue depth.
26
+ * The retry worker is started when registerBridgeTools() is called with an
27
+ * authenticated session.
28
+ */
29
+ import { z } from 'zod';
30
+ import { ingestMarkdownFile, ingestFolder } from './ingest.js';
31
+ import { acquireTokenSilent } from './auth.js';
32
+ import { getQueueStatus, startRetryWorker } from './retry-queue.js';
33
+ // ── Slug isolation helpers (STORY-009, ADR-005) ───────────────────────────────
34
+ /**
35
+ * Asserts that a backend response belongs to the bound client slug.
36
+ *
37
+ * The backend may return a `client_slug` or `slug` field in its JSON response.
38
+ * If the value is present and does NOT match the bound slug, this function throws
39
+ * so the tool handler can reject the response as a cross-client leak.
40
+ *
41
+ * If the response carries no slug field (e.g. a list result or an error body),
42
+ * the check is skipped — the absence of a conflicting slug is not a violation.
43
+ *
44
+ * @param responseSlug The slug found in the backend response (may be undefined).
45
+ * @param boundSlug The slug this bridge instance is bound to.
46
+ * @throws Error if responseSlug is present and does not match boundSlug.
47
+ */
48
+ export function assertResponseSlug(responseSlug, boundSlug) {
49
+ if (responseSlug !== undefined && responseSlug !== boundSlug) {
50
+ throw new Error(`[sound-connect] Isolation violation: response slug "${responseSlug}" ` +
51
+ `does not match bound slug "${boundSlug}". ` +
52
+ 'This instance is locked to one client. Start a separate bridge instance for a different client.');
53
+ }
54
+ }
55
+ /**
56
+ * Parses a JSON response body from the backend and asserts the slug matches.
57
+ * Returns the parsed body so callers do not have to parse twice.
58
+ *
59
+ * @param text Raw response text from the backend.
60
+ * @param boundSlug The slug this bridge instance is bound to.
61
+ * @returns Parsed JSON object (or null if parsing fails — slug check is skipped).
62
+ */
63
+ export function parseAndAssertResponseSlug(text, boundSlug) {
64
+ let body;
65
+ try {
66
+ body = JSON.parse(text);
67
+ }
68
+ catch {
69
+ // Non-JSON response — cannot check slug, pass through.
70
+ return null;
71
+ }
72
+ if (body !== null && typeof body === 'object') {
73
+ const asRecord = body;
74
+ const responseSlug = typeof asRecord['client_slug'] === 'string'
75
+ ? asRecord['client_slug']
76
+ : typeof asRecord['slug'] === 'string'
77
+ ? asRecord['slug']
78
+ : undefined;
79
+ assertResponseSlug(responseSlug, boundSlug);
80
+ }
81
+ return body;
82
+ }
83
+ /**
84
+ * MCP error returned when the peer has not run `login` yet.
85
+ * STORY-010 AC1: exact wording specifies the terminal command to run.
86
+ */
87
+ const NOT_SIGNED_IN_TEXT = 'Not signed in — run `npx @soundbi/sound-connect login` in a terminal.';
88
+ /**
89
+ * Shown when authenticated but the tool is not yet implemented (STORY-008).
90
+ * Distinct from the not-signed-in error so the user knows auth succeeded.
91
+ */
92
+ const NOT_YET_IMPLEMENTED_TEXT = 'Tool not yet implemented in this release. Authentication is working — ' +
93
+ 'full proxy functionality is coming in the next update.';
94
+ function notSignedInResult() {
95
+ return {
96
+ content: [{ type: 'text', text: NOT_SIGNED_IN_TEXT }],
97
+ isError: true,
98
+ };
99
+ }
100
+ function notImplementedResult() {
101
+ return {
102
+ content: [{ type: 'text', text: NOT_YET_IMPLEMENTED_TEXT }],
103
+ isError: true,
104
+ };
105
+ }
106
+ /**
107
+ * Register bridge tools on the MCP server.
108
+ *
109
+ * All tools are visible in tools/list immediately (required by Claude Desktop/Code
110
+ * to show the server as functional). When `isAuthenticated` is false, every tool
111
+ * call returns the NOT_SIGNED_IN_TEXT error — never blocks or hangs (STORY-010).
112
+ * When `isAuthenticated` is true, tool calls return NOT_YET_IMPLEMENTED_TEXT until
113
+ * STORY-008 wires real proxy behaviour.
114
+ *
115
+ * STORY-009 / ADR-005: There is deliberately NO "select_client", "switch_client",
116
+ * or equivalent tool registered here. The bound slug is fixed at process start and
117
+ * cannot be changed in-session. Any outbound call that receives a response for a
118
+ * different slug must call assertResponseSlug() / parseAndAssertResponseSlug() to
119
+ * reject the response before returning it to Claude.
120
+ *
121
+ * @param server The McpServer instance.
122
+ * @param config Bridge config (backendUrl, clientSlug) — fixed at startup.
123
+ * @param isAuthenticated Whether a valid cached token was found at startup (STORY-010).
124
+ */
125
+ export function registerBridgeTools(server, config, isAuthenticated) {
126
+ const { clientSlug } = config;
127
+ // Helper: return the appropriate error based on auth state.
128
+ const guardResult = () => isAuthenticated ? notImplementedResult() : notSignedInResult();
129
+ // STORY-013 / ADR-011: Start the background retry worker when authenticated.
130
+ // The worker drains the local queue with exponential backoff.
131
+ // tokenProvider uses acquireTokenSilent — never blocks or triggers device-code flow.
132
+ if (isAuthenticated) {
133
+ startRetryWorker(clientSlug, async () => {
134
+ try {
135
+ const result = await acquireTokenSilent();
136
+ return result?.accessToken ?? null;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ });
142
+ }
143
+ server.tool('sc_status', `Returns the current connection status for the ${clientSlug} Sound Connect workspace. ` +
144
+ 'Shows whether you are authenticated and which backend this bridge is pointed at.', {}, async () => ({
145
+ content: [{
146
+ type: 'text',
147
+ text: JSON.stringify({
148
+ status: isAuthenticated ? 'authenticated' : 'unauthenticated',
149
+ client_slug: clientSlug,
150
+ hint: isAuthenticated
151
+ ? 'Connected. Full tool proxy available in the next release.'
152
+ : NOT_SIGNED_IN_TEXT,
153
+ }, null, 2),
154
+ }],
155
+ }));
156
+ server.tool('sc_login', 'Shows the current authentication status. ' +
157
+ 'To sign in, run `npx @soundbi/sound-connect login` in a terminal ' +
158
+ '(device-code login cannot be triggered from inside Claude).', {}, async () => ({
159
+ content: [{
160
+ type: 'text',
161
+ text: isAuthenticated
162
+ ? 'Already signed in. No action needed.'
163
+ : NOT_SIGNED_IN_TEXT,
164
+ }],
165
+ isError: !isAuthenticated,
166
+ }));
167
+ server.tool('add_client_memory', `Log a memory, decision, or finding for the ${clientSlug} workspace. Requires authentication.`, {
168
+ category: z.string().describe('Memory category (project, technical, decision, red-flag, client)'),
169
+ title: z.string().describe('Short title'),
170
+ body: z.string().describe('Full memory content'),
171
+ }, async () => guardResult());
172
+ server.tool('search_client_memories', `Search memories and knowledge in the ${clientSlug} workspace. Requires authentication.`, {
173
+ query: z.string().describe('Search query'),
174
+ }, async () => guardResult());
175
+ server.tool('get_client_context', 'Returns which client this bridge is operating on and the current auth state.', {}, async () => ({
176
+ content: [{
177
+ type: 'text',
178
+ text: JSON.stringify({
179
+ client_slug: clientSlug,
180
+ authenticated: isAuthenticated,
181
+ hint: isAuthenticated
182
+ ? 'Connected.'
183
+ : NOT_SIGNED_IN_TEXT,
184
+ }, null, 2),
185
+ }],
186
+ }));
187
+ // ── STORY-011: ingest_file ─────────────────────────────────────────────────
188
+ //
189
+ // Reads a local markdown or transcript file, normalizes, chunks, hashes,
190
+ // attaches provenance, and POSTs to /ingest/:slug.
191
+ // ADR-006: .md + .txt/.vtt/.srt in v1; binary formats deferred to v1.1.
192
+ // ADR-004: sha256 idempotency key; provenance required on every ingest.
193
+ server.tool('ingest_file', `Ingest a local markdown or transcript file into the ${clientSlug} Sound Connect knowledge corpus. ` +
194
+ 'Reads the file, computes a content hash for deduplication, attaches provenance, ' +
195
+ 'and uploads to the backend. Returns a summary of chunks ingested and any deduped chunks. ' +
196
+ 'Supported formats: markdown (.md) and transcript plaintext (.txt, .vtt, .srt).', {
197
+ path: z.string().describe('Absolute or relative path to the markdown or transcript file to ingest'),
198
+ workstream_slug: z
199
+ .string()
200
+ .optional()
201
+ .describe('Optional workstream to scope this knowledge to'),
202
+ }, async (args) => {
203
+ // Auth guard — never attempt ingest without a valid token.
204
+ if (!isAuthenticated) {
205
+ return notSignedInResult();
206
+ }
207
+ // Acquire a fresh token per call (MSAL handles silent refresh).
208
+ let token = null;
209
+ try {
210
+ const result = await acquireTokenSilent();
211
+ token = result?.accessToken ?? null;
212
+ }
213
+ catch {
214
+ token = null;
215
+ }
216
+ if (!token) {
217
+ return {
218
+ content: [{ type: 'text', text: NOT_SIGNED_IN_TEXT }],
219
+ isError: true,
220
+ };
221
+ }
222
+ try {
223
+ // STORY-013: queueOnFailure=true so transient backend failures (network/5xx)
224
+ // queue the chunk locally for retry instead of throwing.
225
+ const summary = await ingestMarkdownFile({
226
+ filePath: args.path,
227
+ backendUrl: config.backendUrl,
228
+ clientSlug,
229
+ token,
230
+ workstreamSlug: args.workstream_slug,
231
+ queueOnFailure: true,
232
+ });
233
+ return {
234
+ content: [{
235
+ type: 'text',
236
+ text: JSON.stringify(summary, null, 2),
237
+ }],
238
+ };
239
+ }
240
+ catch (err) {
241
+ const msg = (err instanceof Error) ? err.message : String(err);
242
+ return {
243
+ content: [{ type: 'text', text: `ingest_file error: ${msg}` }],
244
+ isError: true,
245
+ };
246
+ }
247
+ });
248
+ // ── STORY-012: ingest_folder ───────────────────────────────────────────────
249
+ //
250
+ // Bulk-ingests a directory of .md and transcript files (.txt/.vtt/.srt).
251
+ // Each file flows through the STORY-011 ingest path (ingestMarkdownFile).
252
+ // Idempotency: re-running skips already-ingested files (sha256 dedup, ADR-004).
253
+ // Returns a per-file result table with status: ingested / deduped / failed.
254
+ // ADR-006: .md + transcript plaintext only; binary formats deferred to v1.1.
255
+ // ADR-011: per-file failures captured in results table, do not abort the run.
256
+ server.tool('ingest_folder', `Bulk-ingest a directory of markdown and transcript files into the ${clientSlug} ` +
257
+ 'Sound Connect knowledge corpus. ' +
258
+ 'Enumerates .md, .txt, .vtt, and .srt files in the folder (flat, non-recursive). ' +
259
+ 'Each file is deduplicated by content hash — re-running the folder safely skips already-ingested files. ' +
260
+ 'Returns a per-file result table showing ingested, deduped, or failed status.', {
261
+ path: z.string().describe('Absolute or relative path to the folder to ingest'),
262
+ glob: z
263
+ .string()
264
+ .optional()
265
+ .describe('Optional extension filter, e.g. "*.md" or "*.vtt". ' +
266
+ 'Defaults to all supported types: .md, .txt, .vtt, .srt.'),
267
+ workstream_slug: z
268
+ .string()
269
+ .optional()
270
+ .describe('Optional workstream to scope all ingested files to'),
271
+ }, async (args) => {
272
+ // Auth guard.
273
+ if (!isAuthenticated) {
274
+ return notSignedInResult();
275
+ }
276
+ // Acquire a fresh token per call (MSAL handles silent refresh).
277
+ let token = null;
278
+ try {
279
+ const result = await acquireTokenSilent();
280
+ token = result?.accessToken ?? null;
281
+ }
282
+ catch {
283
+ token = null;
284
+ }
285
+ if (!token) {
286
+ return {
287
+ content: [{ type: 'text', text: NOT_SIGNED_IN_TEXT }],
288
+ isError: true,
289
+ };
290
+ }
291
+ try {
292
+ // STORY-013: queueOnFailure is not yet threaded into ingestFolder directly
293
+ // because ingestFolder calls ingestMarkdownFile per-file without that flag.
294
+ // Transient failures at the file level will surface as "failed" in the results
295
+ // table (ADR-011 per-file capture). The retry queue path is available via
296
+ // ingest_file for individual files when the backend is unreachable.
297
+ const result = await ingestFolder({
298
+ folderPath: args.path,
299
+ glob: args.glob,
300
+ backendUrl: config.backendUrl,
301
+ clientSlug,
302
+ token,
303
+ workstreamSlug: args.workstream_slug,
304
+ });
305
+ return {
306
+ content: [{
307
+ type: 'text',
308
+ text: JSON.stringify(result, null, 2),
309
+ }],
310
+ };
311
+ }
312
+ catch (err) {
313
+ const msg = (err instanceof Error) ? err.message : String(err);
314
+ return {
315
+ content: [{ type: 'text', text: `ingest_folder error: ${msg}` }],
316
+ isError: true,
317
+ };
318
+ }
319
+ });
320
+ // ── STORY-013: ingest_status ───────────────────────────────────────────────
321
+ //
322
+ // Reports the current state of the local retry queue for this client:
323
+ // - pending: chunks queued due to transient failures, awaiting retry
324
+ // - dead: chunks permanently failed (4xx auth/access errors)
325
+ //
326
+ // ADR-011: permanently-failed items (4xx) are surfaced here, not silently retried.
327
+ // The retry worker drains pending items automatically with exponential backoff.
328
+ server.tool('ingest_status', `Report the status of the local ingest retry queue for the ${clientSlug} workspace. ` +
329
+ 'Shows how many chunks are pending retry (transient backend failure) and how many ' +
330
+ 'have permanently failed (auth/access errors that need user action). ' +
331
+ 'The retry worker automatically drains pending items with exponential backoff.', {}, async () => {
332
+ try {
333
+ const status = await getQueueStatus(clientSlug);
334
+ const summary = {
335
+ client_slug: clientSlug,
336
+ pending_chunks: status.pending,
337
+ dead_chunks: status.dead,
338
+ ...(status.dead > 0 ? {
339
+ dead_items: status.deadItems,
340
+ hint: 'Dead items require action — likely an auth error. ' +
341
+ 'Run `npx @soundbi/sound-connect login` to re-authenticate, ' +
342
+ 'then retry the affected files manually.',
343
+ } : {}),
344
+ ...(status.pending > 0 ? {
345
+ pending_items: status.pendingItems,
346
+ hint_pending: 'Pending items will be retried automatically with exponential backoff.',
347
+ } : {}),
348
+ ...(status.pending === 0 && status.dead === 0 ? {
349
+ hint: 'Queue is empty — all ingestion is up to date.',
350
+ } : {}),
351
+ };
352
+ return {
353
+ content: [{
354
+ type: 'text',
355
+ text: JSON.stringify(summary, null, 2),
356
+ }],
357
+ };
358
+ }
359
+ catch (err) {
360
+ const msg = (err instanceof Error) ? err.message : String(err);
361
+ return {
362
+ content: [{ type: 'text', text: `ingest_status error: ${msg}` }],
363
+ isError: true,
364
+ };
365
+ }
366
+ });
367
+ }
368
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@soundbi/sound-connect",
3
+ "version": "0.1.0",
4
+ "description": "Local stdio MCP bridge for Sound Connect — connects Claude Desktop and Claude Code to your Sound Connect workspace.",
5
+ "type": "module",
6
+ "bin": {
7
+ "sound-connect": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx watch src/index.ts",
15
+ "typecheck": "tsc --noEmit",
16
+ "start": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
19
+ },
20
+ "dependencies": {
21
+ "@azure/msal-node": "^5.2.2",
22
+ "@modelcontextprotocol/sdk": "^1.10.0",
23
+ "keytar": "^7.9.0",
24
+ "zod": "^3.23.8"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "@vitest/coverage-v8": "^2.1.9",
29
+ "tsx": "^4.19.2",
30
+ "typescript": "^5.4.5",
31
+ "vitest": "^2.1.9"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ }
36
+ }