@tacitai/mcp-server 0.1.1 → 0.2.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/dist/auth.d.ts +32 -0
- package/dist/auth.js +64 -0
- package/dist/client.d.ts +60 -5
- package/dist/client.js +234 -53
- package/dist/index.js +128 -106
- package/dist/oauth.d.ts +115 -0
- package/dist/oauth.js +619 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +12 -0
- package/dist/setup.d.ts +28 -5
- package/dist/setup.js +266 -59
- package/dist/tools/agents.d.ts +277 -0
- package/dist/tools/agents.js +335 -0
- package/dist/tools/annotations.d.ts +117 -0
- package/dist/tools/annotations.js +130 -0
- package/dist/tools/artifacts.d.ts +178 -8
- package/dist/tools/artifacts.js +372 -81
- package/dist/tools/chronicle.d.ts +3 -0
- package/dist/tools/chronicle.js +165 -0
- package/dist/tools/connector_write_back.d.ts +3 -0
- package/dist/tools/connector_write_back.js +27 -0
- package/dist/tools/converse.d.ts +10 -0
- package/dist/tools/converse.js +88 -0
- package/dist/tools/crews.d.ts +3 -0
- package/dist/tools/crews.js +115 -0
- package/dist/tools/drafts.d.ts +83 -0
- package/dist/tools/drafts.js +86 -0
- package/dist/tools/errors.d.ts +15 -0
- package/dist/tools/errors.js +21 -0
- package/dist/tools/export.d.ts +54 -0
- package/dist/tools/export.js +55 -0
- package/dist/tools/gm.d.ts +214 -0
- package/dist/tools/gm.js +198 -0
- package/dist/tools/ingest.d.ts +58 -4
- package/dist/tools/ingest.js +109 -76
- package/dist/tools/kb.d.ts +544 -0
- package/dist/tools/kb.js +687 -0
- package/dist/tools/lake-structure.d.ts +3 -0
- package/dist/tools/lake-structure.js +126 -0
- package/dist/tools/lake.d.ts +932 -0
- package/dist/tools/lake.js +1017 -0
- package/dist/tools/move.d.ts +200 -0
- package/dist/tools/move.js +96 -0
- package/dist/tools/outbox.d.ts +274 -0
- package/dist/tools/outbox.js +223 -0
- package/dist/tools/people.d.ts +3 -0
- package/dist/tools/people.js +59 -0
- package/dist/tools/pyramid.d.ts +554 -0
- package/dist/tools/pyramid.js +758 -0
- package/dist/tools/recall.d.ts +2 -0
- package/dist/tools/recall.js +28 -17
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.js +197 -0
- package/dist/tools/research.d.ts +47 -0
- package/dist/tools/research.js +575 -0
- package/dist/tools/search.d.ts +9 -0
- package/dist/tools/search.js +86 -25
- package/dist/tools/sharing.d.ts +63 -0
- package/dist/tools/sharing.js +157 -0
- package/dist/tools/synthesize.d.ts +54 -2
- package/dist/tools/synthesize.js +80 -23
- package/dist/tools/web-url.d.ts +57 -0
- package/dist/tools/web-url.js +74 -0
- package/dist/tools/workspaces.d.ts +3 -0
- package/dist/tools/workspaces.js +47 -0
- package/dist/transport.d.ts +9 -0
- package/dist/transport.js +209 -0
- package/package.json +5 -4
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPS transport auth + rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Security invariants:
|
|
5
|
+
* - Token validation happens at the TACIT API; this module only checks presence.
|
|
6
|
+
* - The Authorization header value is NEVER logged.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage } from 'node:http';
|
|
9
|
+
import { type OAuthTokenInfo } from './oauth.js';
|
|
10
|
+
/** Returns the Bearer token, or null if missing/empty. */
|
|
11
|
+
export declare function authenticateRequest(req: IncomingMessage): string | null;
|
|
12
|
+
export type AuthResult = {
|
|
13
|
+
type: 'bearer';
|
|
14
|
+
token: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'oauth';
|
|
17
|
+
info: OAuthTokenInfo;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'invalid_token';
|
|
20
|
+
} | null;
|
|
21
|
+
/**
|
|
22
|
+
* Dual-auth resolution: discriminates Bearer vs OAuth tokens.
|
|
23
|
+
* OAuth tokens carry the `tmo_` prefix; all others route to Bearer path.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveAuth(req: IncomingMessage): AuthResult;
|
|
26
|
+
export declare class RateLimiter {
|
|
27
|
+
private readonly buckets;
|
|
28
|
+
private readonly limit;
|
|
29
|
+
constructor(limit?: number);
|
|
30
|
+
/** Consumes one token for the IP; returns false when exhausted. */
|
|
31
|
+
tryConsume(ip: string): boolean;
|
|
32
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
/**
|
|
3
|
+
* HTTPS transport auth + rate limiting.
|
|
4
|
+
*
|
|
5
|
+
* Security invariants:
|
|
6
|
+
* - Token validation happens at the TACIT API; this module only checks presence.
|
|
7
|
+
* - The Authorization header value is NEVER logged.
|
|
8
|
+
*/
|
|
9
|
+
import { validateOAuthToken } from './oauth.js';
|
|
10
|
+
/** Returns the Bearer token, or null if missing/empty. */
|
|
11
|
+
export function authenticateRequest(req) {
|
|
12
|
+
const header = req.headers['authorization'];
|
|
13
|
+
if (typeof header !== 'string')
|
|
14
|
+
return null;
|
|
15
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
16
|
+
const token = match?.[1]?.trim();
|
|
17
|
+
return token ? token : null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Dual-auth resolution: discriminates Bearer vs OAuth tokens.
|
|
21
|
+
* OAuth tokens carry the `tmo_` prefix; all others route to Bearer path.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveAuth(req) {
|
|
24
|
+
const header = req.headers['authorization'];
|
|
25
|
+
if (typeof header !== 'string')
|
|
26
|
+
return null;
|
|
27
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
28
|
+
const token = match?.[1]?.trim();
|
|
29
|
+
if (!token)
|
|
30
|
+
return null;
|
|
31
|
+
if (token.startsWith('tmo_')) {
|
|
32
|
+
const info = validateOAuthToken(token);
|
|
33
|
+
if (!info)
|
|
34
|
+
return { type: 'invalid_token' };
|
|
35
|
+
return { type: 'oauth', info };
|
|
36
|
+
}
|
|
37
|
+
return { type: 'bearer', token };
|
|
38
|
+
}
|
|
39
|
+
const REFILL_INTERVAL_MS = 60_000;
|
|
40
|
+
export class RateLimiter {
|
|
41
|
+
buckets = new Map();
|
|
42
|
+
limit;
|
|
43
|
+
constructor(limit) {
|
|
44
|
+
const fromEnv = Number.parseInt(process.env['MCP_RATE_LIMIT_RPM'] ?? '', 10);
|
|
45
|
+
this.limit = limit ?? (Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 100);
|
|
46
|
+
}
|
|
47
|
+
/** Consumes one token for the IP; returns false when exhausted. */
|
|
48
|
+
tryConsume(ip) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
let bucket = this.buckets.get(ip);
|
|
51
|
+
if (!bucket) {
|
|
52
|
+
bucket = { tokens: this.limit, lastRefill: now };
|
|
53
|
+
this.buckets.set(ip, bucket);
|
|
54
|
+
}
|
|
55
|
+
else if (now - bucket.lastRefill >= REFILL_INTERVAL_MS) {
|
|
56
|
+
bucket.tokens = this.limit;
|
|
57
|
+
bucket.lastRefill = now;
|
|
58
|
+
}
|
|
59
|
+
if (bucket.tokens <= 0)
|
|
60
|
+
return false;
|
|
61
|
+
bucket.tokens -= 1;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export interface TacitClientOptions {
|
|
11
11
|
apiUrl?: string;
|
|
12
|
+
appUrl?: string;
|
|
12
13
|
token?: string;
|
|
13
14
|
workspaceId?: string;
|
|
14
15
|
/** Override the root directory used to locate .tacit/sync.json (default: process.cwd()). */
|
|
@@ -16,10 +17,13 @@ export interface TacitClientOptions {
|
|
|
16
17
|
}
|
|
17
18
|
export declare class TacitClient {
|
|
18
19
|
private readonly apiUrl;
|
|
19
|
-
private readonly
|
|
20
|
+
private readonly _appUrl;
|
|
21
|
+
private token;
|
|
20
22
|
private readonly _fetch;
|
|
21
23
|
private readonly _rootDir;
|
|
22
|
-
|
|
24
|
+
private _workspaceId;
|
|
25
|
+
private _workspaceName;
|
|
26
|
+
private _initialized;
|
|
23
27
|
/**
|
|
24
28
|
* @param fetchOrOpts - Optional: a custom fetch implementation (for testing),
|
|
25
29
|
* or a TacitClientOptions object. If a function is provided it overrides
|
|
@@ -28,6 +32,13 @@ export declare class TacitClient {
|
|
|
28
32
|
* Only used when fetchOrOpts is a fetch function.
|
|
29
33
|
*/
|
|
30
34
|
constructor(fetchOrOpts?: typeof globalThis.fetch | TacitClientOptions, opts?: TacitClientOptions);
|
|
35
|
+
get workspaceId(): string;
|
|
36
|
+
get workspaceName(): string | null;
|
|
37
|
+
initialize(): Promise<void>;
|
|
38
|
+
switchWorkspace(workspaceId: string): Promise<{
|
|
39
|
+
workspace_id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}>;
|
|
31
42
|
/**
|
|
32
43
|
* Resolve the workspace ID:
|
|
33
44
|
* 1. Read workspace_id from <rootDir>/.tacit/sync.json if the file exists.
|
|
@@ -35,12 +46,38 @@ export declare class TacitClient {
|
|
|
35
46
|
* 3. Return null if neither source is available.
|
|
36
47
|
*/
|
|
37
48
|
resolveWorkspaceId(): Promise<string | null>;
|
|
38
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Build a user-facing URL for an entity in the TACIT web app.
|
|
51
|
+
* Returns null when TACIT_APP_URL is not configured.
|
|
52
|
+
*
|
|
53
|
+
* Route patterns (TanStack file-based routing in tacitai.app.beta):
|
|
54
|
+
* artifact → /view/:id
|
|
55
|
+
* kb_article → /knowledge/:collectionId/:articleId
|
|
56
|
+
* dossier → /dossiers (list; no single-dossier route yet)
|
|
57
|
+
* guest → /guest/view/:id
|
|
58
|
+
*/
|
|
59
|
+
webUrl(entityType: string, id: string, extra?: {
|
|
60
|
+
collectionId?: string;
|
|
61
|
+
}): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Build auth headers. Value is NEVER written to logs.
|
|
64
|
+
*
|
|
65
|
+
* MV-1515n (W5 harness): when TACIT_SESSION_TOKEN is set (an HMAC
|
|
66
|
+
* X-Tacit-Session token, per tacit-internal-token-auth §HMAC session token
|
|
67
|
+
* format), it takes precedence and is sent as X-Tacit-Session instead of
|
|
68
|
+
* Authorization — this is additive and does not change default behavior
|
|
69
|
+
* (Bearer via TACIT_TOKEN) when TACIT_SESSION_TOKEN is unset.
|
|
70
|
+
*/
|
|
39
71
|
private authHeaders;
|
|
40
72
|
/** GET request returning parsed JSON. */
|
|
41
73
|
get<T = unknown>(endpoint: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
42
|
-
/** POST request returning parsed JSON. */
|
|
43
|
-
post<T = unknown>(endpoint: string, body: unknown): Promise<T>;
|
|
74
|
+
/** POST request returning parsed JSON. `extraHeaders` merges on top of auth headers (e.g. If-Match). */
|
|
75
|
+
post<T = unknown>(endpoint: string, body: unknown, extraHeaders?: Record<string, string>): Promise<T>;
|
|
76
|
+
/** PUT request returning parsed JSON. */
|
|
77
|
+
put<T = unknown>(endpoint: string, body: unknown): Promise<T>;
|
|
78
|
+
/** DELETE request returning parsed JSON (or empty on 204). */
|
|
79
|
+
delete<T = unknown>(endpoint: string): Promise<T | null>;
|
|
80
|
+
request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
|
44
81
|
/** PATCH request returning parsed JSON. */
|
|
45
82
|
patch<T = unknown>(endpoint: string, body: unknown): Promise<T>;
|
|
46
83
|
/**
|
|
@@ -57,4 +94,22 @@ export declare class TacitApiError extends Error {
|
|
|
57
94
|
readonly endpoint: string;
|
|
58
95
|
constructor(statusCode: number, body: string, endpoint: string);
|
|
59
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Thrown when the API returns 401 — indicates the developer token has expired
|
|
99
|
+
* or been revoked. Tools catch this and surface a user-actionable message.
|
|
100
|
+
*
|
|
101
|
+
* FEAT-193: The server grants a 7-day grace window after expiry — during that
|
|
102
|
+
* window, `POST /api/v1/developer-tokens/refresh` still works (no browser
|
|
103
|
+
* re-auth needed). After 7 days, full device-auth is required.
|
|
104
|
+
*
|
|
105
|
+
* Security invariant: token plaintext is NEVER included in this error.
|
|
106
|
+
*/
|
|
107
|
+
export declare class TokenExpiredError extends Error {
|
|
108
|
+
readonly renewalUrl: string;
|
|
109
|
+
readonly setupCommand: string;
|
|
110
|
+
constructor(renewalUrl: string, setupCommand: string);
|
|
111
|
+
}
|
|
112
|
+
/** HTTPS mode: bind the caller's token/apiUrl/principal to everything run inside fn. */
|
|
113
|
+
export declare function runWithRequestContext<T>(token: string, apiUrl: string, fn: () => Promise<T>, principal?: string): Promise<T>;
|
|
114
|
+
export declare function getRequestPrincipal(): string | null;
|
|
60
115
|
export declare function getClient(): TacitClient;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// SPDX-License-Identifier: MIT
|
|
3
2
|
/**
|
|
4
3
|
* TacitClient — REST/SSE client over TACIT_API_URL.
|
|
@@ -9,50 +8,22 @@
|
|
|
9
8
|
* - No anthropic/openai SDK import.
|
|
10
9
|
* - Token sourced exclusively from process.env.TACIT_TOKEN.
|
|
11
10
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (k2 === undefined) k2 = k;
|
|
21
|
-
o[k2] = m[k];
|
|
22
|
-
}));
|
|
23
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
-
}) : function(o, v) {
|
|
26
|
-
o["default"] = v;
|
|
27
|
-
});
|
|
28
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
-
var ownKeys = function(o) {
|
|
30
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
-
var ar = [];
|
|
32
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
-
return ar;
|
|
34
|
-
};
|
|
35
|
-
return ownKeys(o);
|
|
36
|
-
};
|
|
37
|
-
return function (mod) {
|
|
38
|
-
if (mod && mod.__esModule) return mod;
|
|
39
|
-
var result = {};
|
|
40
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
-
__setModuleDefault(result, mod);
|
|
42
|
-
return result;
|
|
43
|
-
};
|
|
44
|
-
})();
|
|
45
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
-
exports.TacitApiError = exports.TacitClient = void 0;
|
|
47
|
-
exports.getClient = getClient;
|
|
48
|
-
const fs = __importStar(require("fs"));
|
|
49
|
-
const path = __importStar(require("path"));
|
|
50
|
-
class TacitClient {
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
|
+
function isHmacSessionToken(token) {
|
|
15
|
+
const parts = token.split('.');
|
|
16
|
+
return parts.length === 2 && /^[0-9a-f]{64}$/.test(parts[1]);
|
|
17
|
+
}
|
|
18
|
+
export class TacitClient {
|
|
51
19
|
apiUrl;
|
|
20
|
+
_appUrl;
|
|
52
21
|
token;
|
|
53
22
|
_fetch;
|
|
54
23
|
_rootDir;
|
|
55
|
-
|
|
24
|
+
_workspaceId;
|
|
25
|
+
_workspaceName = null;
|
|
26
|
+
_initialized = false;
|
|
56
27
|
/**
|
|
57
28
|
* @param fetchOrOpts - Optional: a custom fetch implementation (for testing),
|
|
58
29
|
* or a TacitClientOptions object. If a function is provided it overrides
|
|
@@ -72,6 +43,7 @@ class TacitClient {
|
|
|
72
43
|
this._fetch = globalThis.fetch.bind(globalThis);
|
|
73
44
|
}
|
|
74
45
|
this.apiUrl = (resolvedOpts.apiUrl ?? process.env['TACIT_API_URL'] ?? 'http://localhost:7473').replace(/\/$/, '');
|
|
46
|
+
this._appUrl = (resolvedOpts.appUrl ?? process.env['TACIT_APP_URL'] ?? '').replace(/\/$/, '');
|
|
75
47
|
// Token from env only — NEVER log this value.
|
|
76
48
|
this.token = resolvedOpts.token ?? process.env['TACIT_TOKEN'] ?? '';
|
|
77
49
|
this._rootDir = resolvedOpts.rootDir ?? process.cwd();
|
|
@@ -90,7 +62,63 @@ class TacitClient {
|
|
|
90
62
|
catch {
|
|
91
63
|
// Soft-coupled: ignore read errors; fall back to env.
|
|
92
64
|
}
|
|
93
|
-
this.
|
|
65
|
+
this._workspaceId = resolvedWorkspaceId;
|
|
66
|
+
}
|
|
67
|
+
get workspaceId() {
|
|
68
|
+
return this._workspaceId;
|
|
69
|
+
}
|
|
70
|
+
get workspaceName() {
|
|
71
|
+
return this._workspaceName;
|
|
72
|
+
}
|
|
73
|
+
async initialize() {
|
|
74
|
+
if (this._initialized)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
const data = await this.get('/api/v1/workspaces/mine');
|
|
78
|
+
if (data && typeof data === 'object') {
|
|
79
|
+
if ('active_workspace_id' in data && typeof data.active_workspace_id === 'string') {
|
|
80
|
+
this._workspaceId = data.active_workspace_id;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(data.workspaces) && data.workspaces.length > 0) {
|
|
83
|
+
const active = data.workspaces.find((w) => w.workspace_id === this._workspaceId || w.id === this._workspaceId);
|
|
84
|
+
if (active) {
|
|
85
|
+
const name = active.display_name ?? active.name;
|
|
86
|
+
if (typeof name === 'string')
|
|
87
|
+
this._workspaceName = name;
|
|
88
|
+
}
|
|
89
|
+
if (!this._workspaceId) {
|
|
90
|
+
const first = data.workspaces[0];
|
|
91
|
+
this._workspaceId = (first.workspace_id ?? first.id);
|
|
92
|
+
const name = first.display_name ?? first.name;
|
|
93
|
+
if (typeof name === 'string')
|
|
94
|
+
this._workspaceName = name;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this._initialized = true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
this._initialized = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async switchWorkspace(workspaceId) {
|
|
105
|
+
const data = await this.post('/api/v1/auth/switch-workspace', {
|
|
106
|
+
workspace_id: workspaceId,
|
|
107
|
+
});
|
|
108
|
+
if (data && typeof data === 'object' && 'token' in data && typeof data.token === 'string') {
|
|
109
|
+
this.token = data.token;
|
|
110
|
+
}
|
|
111
|
+
this._workspaceId = workspaceId;
|
|
112
|
+
if (data && typeof data === 'object' && 'name' in data && typeof data.name === 'string') {
|
|
113
|
+
this._workspaceName = data.name;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this._workspaceName = null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
workspace_id: workspaceId,
|
|
120
|
+
name: this._workspaceName ?? 'unknown',
|
|
121
|
+
};
|
|
94
122
|
}
|
|
95
123
|
/**
|
|
96
124
|
* Resolve the workspace ID:
|
|
@@ -114,15 +142,71 @@ class TacitClient {
|
|
|
114
142
|
const envId = process.env['TACIT_WORKSPACE_ID'];
|
|
115
143
|
return (envId && envId.length > 0) ? envId : null;
|
|
116
144
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Build a user-facing URL for an entity in the TACIT web app.
|
|
147
|
+
* Returns null when TACIT_APP_URL is not configured.
|
|
148
|
+
*
|
|
149
|
+
* Route patterns (TanStack file-based routing in tacitai.app.beta):
|
|
150
|
+
* artifact → /view/:id
|
|
151
|
+
* kb_article → /knowledge/:collectionId/:articleId
|
|
152
|
+
* dossier → /dossiers (list; no single-dossier route yet)
|
|
153
|
+
* guest → /guest/view/:id
|
|
154
|
+
*/
|
|
155
|
+
webUrl(entityType, id, extra) {
|
|
156
|
+
if (!this._appUrl)
|
|
157
|
+
return null;
|
|
158
|
+
switch (entityType) {
|
|
159
|
+
case 'artifact':
|
|
160
|
+
return `${this._appUrl}/view/${id}`;
|
|
161
|
+
case 'kb_article':
|
|
162
|
+
return extra?.collectionId
|
|
163
|
+
? `${this._appUrl}/knowledge/${extra.collectionId}/${id}`
|
|
164
|
+
: null;
|
|
165
|
+
case 'dossier':
|
|
166
|
+
return `${this._appUrl}/dossiers`;
|
|
167
|
+
case 'dossier_summary':
|
|
168
|
+
return `${this._appUrl}/summary/${id}`;
|
|
169
|
+
case 'dossier_scorecard':
|
|
170
|
+
return `${this._appUrl}/scorecard/${id}`;
|
|
171
|
+
case 'dossier_deliverable':
|
|
172
|
+
return extra?.collectionId
|
|
173
|
+
? `${this._appUrl}/deliverables/${extra.collectionId}/${id}`
|
|
174
|
+
: `${this._appUrl}/dossiers`;
|
|
175
|
+
case 'guest':
|
|
176
|
+
return `${this._appUrl}/guest/view/${id}`;
|
|
177
|
+
case 'knowledge':
|
|
178
|
+
return `${this._appUrl}/knowledge`;
|
|
179
|
+
default:
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Build auth headers. Value is NEVER written to logs.
|
|
185
|
+
*
|
|
186
|
+
* MV-1515n (W5 harness): when TACIT_SESSION_TOKEN is set (an HMAC
|
|
187
|
+
* X-Tacit-Session token, per tacit-internal-token-auth §HMAC session token
|
|
188
|
+
* format), it takes precedence and is sent as X-Tacit-Session instead of
|
|
189
|
+
* Authorization — this is additive and does not change default behavior
|
|
190
|
+
* (Bearer via TACIT_TOKEN) when TACIT_SESSION_TOKEN is unset.
|
|
191
|
+
*/
|
|
192
|
+
authHeaders(extra) {
|
|
119
193
|
const headers = {
|
|
120
194
|
'Content-Type': 'application/json',
|
|
121
195
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
headers['
|
|
196
|
+
const sessionToken = process.env['TACIT_SESSION_TOKEN'];
|
|
197
|
+
if (sessionToken) {
|
|
198
|
+
headers['X-Tacit-Session'] = sessionToken;
|
|
199
|
+
}
|
|
200
|
+
else if (this.token) {
|
|
201
|
+
if (isHmacSessionToken(this.token)) {
|
|
202
|
+
headers['X-Tacit-Session'] = this.token;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
206
|
+
}
|
|
125
207
|
}
|
|
208
|
+
if (extra)
|
|
209
|
+
Object.assign(headers, extra);
|
|
126
210
|
return headers;
|
|
127
211
|
}
|
|
128
212
|
/** GET request returning parsed JSON. */
|
|
@@ -140,23 +224,72 @@ class TacitClient {
|
|
|
140
224
|
});
|
|
141
225
|
if (!res.ok) {
|
|
142
226
|
const body = await res.text().catch(() => '');
|
|
227
|
+
if (res.status === 401) {
|
|
228
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
229
|
+
}
|
|
143
230
|
throw new TacitApiError(res.status, body, endpoint);
|
|
144
231
|
}
|
|
145
232
|
return res.json();
|
|
146
233
|
}
|
|
147
|
-
/** POST request returning parsed JSON. */
|
|
148
|
-
async post(endpoint, body) {
|
|
234
|
+
/** POST request returning parsed JSON. `extraHeaders` merges on top of auth headers (e.g. If-Match). */
|
|
235
|
+
async post(endpoint, body, extraHeaders) {
|
|
149
236
|
const res = await this._fetch(`${this.apiUrl}${endpoint}`, {
|
|
150
237
|
method: 'POST',
|
|
238
|
+
headers: this.authHeaders(extraHeaders),
|
|
239
|
+
body: JSON.stringify(body),
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
const text = await res.text().catch(() => '');
|
|
243
|
+
if (res.status === 401) {
|
|
244
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
245
|
+
}
|
|
246
|
+
throw new TacitApiError(res.status, text, endpoint);
|
|
247
|
+
}
|
|
248
|
+
return res.json();
|
|
249
|
+
}
|
|
250
|
+
/** PUT request returning parsed JSON. */
|
|
251
|
+
async put(endpoint, body) {
|
|
252
|
+
const res = await this._fetch(`${this.apiUrl}${endpoint}`, {
|
|
253
|
+
method: 'PUT',
|
|
151
254
|
headers: this.authHeaders(),
|
|
152
255
|
body: JSON.stringify(body),
|
|
153
256
|
});
|
|
154
257
|
if (!res.ok) {
|
|
155
258
|
const text = await res.text().catch(() => '');
|
|
259
|
+
if (res.status === 401) {
|
|
260
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
261
|
+
}
|
|
156
262
|
throw new TacitApiError(res.status, text, endpoint);
|
|
157
263
|
}
|
|
158
264
|
return res.json();
|
|
159
265
|
}
|
|
266
|
+
/** DELETE request returning parsed JSON (or empty on 204). */
|
|
267
|
+
async delete(endpoint) {
|
|
268
|
+
const res = await this._fetch(`${this.apiUrl}${endpoint}`, {
|
|
269
|
+
method: 'DELETE',
|
|
270
|
+
headers: this.authHeaders(),
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
const text = await res.text().catch(() => '');
|
|
274
|
+
if (res.status === 401) {
|
|
275
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
276
|
+
}
|
|
277
|
+
throw new TacitApiError(res.status, text, endpoint);
|
|
278
|
+
}
|
|
279
|
+
if (res.status === 204)
|
|
280
|
+
return null;
|
|
281
|
+
return res.json();
|
|
282
|
+
}
|
|
283
|
+
async request(method, path, body) {
|
|
284
|
+
switch (method.toUpperCase()) {
|
|
285
|
+
case 'GET': return this.get(path);
|
|
286
|
+
case 'POST': return this.post(path, body);
|
|
287
|
+
case 'PUT': return this.put(path, body);
|
|
288
|
+
case 'PATCH': return this.patch(path, body);
|
|
289
|
+
case 'DELETE': return this.delete(path);
|
|
290
|
+
default: throw new Error(`Unsupported HTTP method: ${method}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
160
293
|
/** PATCH request returning parsed JSON. */
|
|
161
294
|
async patch(endpoint, body) {
|
|
162
295
|
const res = await this._fetch(`${this.apiUrl}${endpoint}`, {
|
|
@@ -166,8 +299,14 @@ class TacitClient {
|
|
|
166
299
|
});
|
|
167
300
|
if (!res.ok) {
|
|
168
301
|
const text = await res.text().catch(() => '');
|
|
302
|
+
if (res.status === 401) {
|
|
303
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
304
|
+
}
|
|
169
305
|
throw new TacitApiError(res.status, text, endpoint);
|
|
170
306
|
}
|
|
307
|
+
if (res.status === 204) {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
171
310
|
return res.json();
|
|
172
311
|
}
|
|
173
312
|
/**
|
|
@@ -185,6 +324,9 @@ class TacitClient {
|
|
|
185
324
|
});
|
|
186
325
|
if (!res.ok) {
|
|
187
326
|
const text = await res.text().catch(() => '');
|
|
327
|
+
if (res.status === 401) {
|
|
328
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
329
|
+
}
|
|
188
330
|
throw new TacitApiError(res.status, text, endpoint);
|
|
189
331
|
}
|
|
190
332
|
if (!res.body) {
|
|
@@ -236,13 +378,15 @@ class TacitClient {
|
|
|
236
378
|
});
|
|
237
379
|
if (!res.ok) {
|
|
238
380
|
const text = await res.text().catch(() => '');
|
|
381
|
+
if (res.status === 401) {
|
|
382
|
+
throw new TokenExpiredError(`${this.apiUrl}/settings`, `npx @tacitai/mcp-server setup --api-url ${this.apiUrl}`);
|
|
383
|
+
}
|
|
239
384
|
throw new TacitApiError(res.status, text, endpoint);
|
|
240
385
|
}
|
|
241
386
|
return res.json();
|
|
242
387
|
}
|
|
243
388
|
}
|
|
244
|
-
|
|
245
|
-
class TacitApiError extends Error {
|
|
389
|
+
export class TacitApiError extends Error {
|
|
246
390
|
statusCode;
|
|
247
391
|
body;
|
|
248
392
|
endpoint;
|
|
@@ -254,10 +398,47 @@ class TacitApiError extends Error {
|
|
|
254
398
|
this.name = 'TacitApiError';
|
|
255
399
|
}
|
|
256
400
|
}
|
|
257
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Thrown when the API returns 401 — indicates the developer token has expired
|
|
403
|
+
* or been revoked. Tools catch this and surface a user-actionable message.
|
|
404
|
+
*
|
|
405
|
+
* FEAT-193: The server grants a 7-day grace window after expiry — during that
|
|
406
|
+
* window, `POST /api/v1/developer-tokens/refresh` still works (no browser
|
|
407
|
+
* re-auth needed). After 7 days, full device-auth is required.
|
|
408
|
+
*
|
|
409
|
+
* Security invariant: token plaintext is NEVER included in this error.
|
|
410
|
+
*/
|
|
411
|
+
export class TokenExpiredError extends Error {
|
|
412
|
+
renewalUrl;
|
|
413
|
+
setupCommand;
|
|
414
|
+
constructor(renewalUrl, setupCommand) {
|
|
415
|
+
const msg = [
|
|
416
|
+
'Developer token expired or revoked.',
|
|
417
|
+
'Re-authenticate within 7 days for easy refresh (no browser flow needed).',
|
|
418
|
+
`Renew at: ${renewalUrl}`,
|
|
419
|
+
`Re-run setup: ${setupCommand}`,
|
|
420
|
+
].join('\n');
|
|
421
|
+
super(msg);
|
|
422
|
+
this.renewalUrl = renewalUrl;
|
|
423
|
+
this.setupCommand = setupCommand;
|
|
424
|
+
this.name = 'TokenExpiredError';
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const requestStore = new AsyncLocalStorage();
|
|
428
|
+
/** HTTPS mode: bind the caller's token/apiUrl/principal to everything run inside fn. */
|
|
429
|
+
export function runWithRequestContext(token, apiUrl, fn, principal) {
|
|
430
|
+
return requestStore.run({ token, apiUrl, principal: principal ?? 'anonymous' }, fn);
|
|
431
|
+
}
|
|
432
|
+
export function getRequestPrincipal() {
|
|
433
|
+
return requestStore.getStore()?.principal ?? null;
|
|
434
|
+
}
|
|
258
435
|
/** Shared singleton client (initialized lazily on first use). */
|
|
259
436
|
let _defaultClient = null;
|
|
260
|
-
function getClient() {
|
|
437
|
+
export function getClient() {
|
|
438
|
+
const ctx = requestStore.getStore();
|
|
439
|
+
if (ctx) {
|
|
440
|
+
return new TacitClient({ apiUrl: ctx.apiUrl, token: ctx.token });
|
|
441
|
+
}
|
|
261
442
|
if (!_defaultClient) {
|
|
262
443
|
_defaultClient = new TacitClient();
|
|
263
444
|
}
|