@neuraltrust/trustgate 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 (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +132 -0
  3. package/dist/agent.d.ts +156 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +216 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/client.d.ts +88 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +152 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +44 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +40 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/connections.d.ts +13 -0
  16. package/dist/connections.d.ts.map +1 -0
  17. package/dist/connections.js +46 -0
  18. package/dist/connections.js.map +1 -0
  19. package/dist/errors.d.ts +91 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +144 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/formats.d.ts +44 -0
  24. package/dist/formats.d.ts.map +1 -0
  25. package/dist/formats.js +299 -0
  26. package/dist/formats.js.map +1 -0
  27. package/dist/http.d.ts +23 -0
  28. package/dist/http.d.ts.map +1 -0
  29. package/dist/http.js +94 -0
  30. package/dist/http.js.map +1 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +10 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/mcp.d.ts +43 -0
  36. package/dist/mcp.d.ts.map +1 -0
  37. package/dist/mcp.js +175 -0
  38. package/dist/mcp.js.map +1 -0
  39. package/dist/schema.d.ts +52 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +168 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/types.d.ts +78 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +52 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/whoami.d.ts +73 -0
  48. package/dist/whoami.d.ts.map +1 -0
  49. package/dist/whoami.js +79 -0
  50. package/dist/whoami.js.map +1 -0
  51. package/package.json +29 -0
  52. package/src/agent.ts +267 -0
  53. package/src/client.ts +203 -0
  54. package/src/config.ts +78 -0
  55. package/src/connections.ts +90 -0
  56. package/src/errors.ts +154 -0
  57. package/src/formats.ts +362 -0
  58. package/src/http.ts +106 -0
  59. package/src/index.ts +41 -0
  60. package/src/mcp.ts +203 -0
  61. package/src/schema.ts +193 -0
  62. package/src/types.ts +98 -0
  63. package/src/whoami.ts +172 -0
@@ -0,0 +1,78 @@
1
+ /** Which actor a handle speaks as. The consumer decides this, not the caller. */
2
+ export declare const Actor: {
3
+ /** The application itself — principal `app:<consumer_id>`. */
4
+ readonly Application: "application";
5
+ /** One of the application's own end users. */
6
+ readonly EndUser: "end_user";
7
+ };
8
+ export type Actor = (typeof Actor)[keyof typeof Actor];
9
+ /**
10
+ * The tool shapes the SDK can emit.
11
+ *
12
+ * These are model providers, not agent frameworks. A framework brings its own
13
+ * MCP client, so it takes the gateway's URL and lists the tools itself — there
14
+ * is nothing to convert. Conversion only happens when you call a provider's
15
+ * API directly, which is why nothing here is called "langchain".
16
+ */
17
+ export declare const ToolFormat: {
18
+ /** OpenAI Responses API — function tools, flattened. */
19
+ readonly OpenAIResponses: "openai-responses";
20
+ /** OpenAI Chat Completions — function tools, nested under `function`. */
21
+ readonly OpenAIChat: "openai-chat";
22
+ /** Anthropic Messages — `input_schema`. */
23
+ readonly AnthropicMessages: "anthropic-messages";
24
+ /** Google Gemini — a single `functionDeclarations` entry. */
25
+ readonly Gemini: "gemini";
26
+ };
27
+ export type ToolFormat = (typeof ToolFormat)[keyof typeof ToolFormat];
28
+ /** A tool as the gateway serves it, before any provider dialect is applied. */
29
+ export type GatewayTool = {
30
+ name: string;
31
+ title?: string;
32
+ description?: string;
33
+ inputSchema: JSONSchema;
34
+ outputSchema?: JSONSchema;
35
+ };
36
+ export type JSONSchema = Record<string, unknown>;
37
+ /** One upstream account of an actor, as the connections API reports it. */
38
+ export type Connection = {
39
+ provider: string;
40
+ registry?: string;
41
+ code?: string;
42
+ status: 'connected' | 'needs_reconnect' | 'not_connected';
43
+ accountRef?: string;
44
+ expiresAt?: Date;
45
+ };
46
+ /** The link an end user opens to connect their own account. */
47
+ export type ConnectLink = {
48
+ connectUrl: string;
49
+ ticket: string;
50
+ provider?: string;
51
+ expiresAt: Date;
52
+ };
53
+ /** Everything a provider's client needs to reach the gateway. */
54
+ export type Endpoint = {
55
+ url: string;
56
+ headers: Record<string, string>;
57
+ };
58
+ /** A tool call as the SDK understands it, whatever provider asked for it. */
59
+ export type ToolCall = {
60
+ /** The provider's own identifier for this call, echoed back in the result. */
61
+ id: string;
62
+ name: string;
63
+ arguments: Record<string, unknown>;
64
+ };
65
+ /**
66
+ * The exposed name of a tool the caller named.
67
+ *
68
+ * The gateway prefixes every tool with the server it came from, so Linear's
69
+ * `list_issues` is served as `linear_list_issues`. That prefix is the gateway's
70
+ * doing and not the caller's, so a name written without one resolves — as long
71
+ * as exactly one server serves it. Two that do is a genuine question only the
72
+ * caller can answer, and it is asked rather than guessed.
73
+ *
74
+ * A name that matches nothing is returned unchanged, so the error that follows
75
+ * is about the tool rather than about this.
76
+ */
77
+ export declare function resolveToolName(name: string, tools: GatewayTool[]): string;
78
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,iFAAiF;AACjF,eAAO,MAAM,KAAK;IACjB,8DAA8D;;IAE9D,8CAA8C;;CAErC,CAAA;AACV,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAA;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU;IACtB,wDAAwD;;IAExD,yEAAyE;;IAEzE,2CAA2C;;IAE3C,6DAA6D;;CAEpD,CAAA;AACV,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAA;AAErE,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,UAAU,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEhD,2EAA2E;AAC3E,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,GAAG,iBAAiB,GAAG,eAAe,CAAA;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,IAAI,CAAA;CAChB,CAAA;AAED,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,IAAI,CAAA;CACf,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,QAAQ,GAAG;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,GAAG;IACtB,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAY1E"}
package/dist/types.js ADDED
@@ -0,0 +1,52 @@
1
+ import { TrustGateError } from './errors.js';
2
+ /** Which actor a handle speaks as. The consumer decides this, not the caller. */
3
+ export const Actor = {
4
+ /** The application itself — principal `app:<consumer_id>`. */
5
+ Application: 'application',
6
+ /** One of the application's own end users. */
7
+ EndUser: 'end_user',
8
+ };
9
+ /**
10
+ * The tool shapes the SDK can emit.
11
+ *
12
+ * These are model providers, not agent frameworks. A framework brings its own
13
+ * MCP client, so it takes the gateway's URL and lists the tools itself — there
14
+ * is nothing to convert. Conversion only happens when you call a provider's
15
+ * API directly, which is why nothing here is called "langchain".
16
+ */
17
+ export const ToolFormat = {
18
+ /** OpenAI Responses API — function tools, flattened. */
19
+ OpenAIResponses: 'openai-responses',
20
+ /** OpenAI Chat Completions — function tools, nested under `function`. */
21
+ OpenAIChat: 'openai-chat',
22
+ /** Anthropic Messages — `input_schema`. */
23
+ AnthropicMessages: 'anthropic-messages',
24
+ /** Google Gemini — a single `functionDeclarations` entry. */
25
+ Gemini: 'gemini',
26
+ };
27
+ /**
28
+ * The exposed name of a tool the caller named.
29
+ *
30
+ * The gateway prefixes every tool with the server it came from, so Linear's
31
+ * `list_issues` is served as `linear_list_issues`. That prefix is the gateway's
32
+ * doing and not the caller's, so a name written without one resolves — as long
33
+ * as exactly one server serves it. Two that do is a genuine question only the
34
+ * caller can answer, and it is asked rather than guessed.
35
+ *
36
+ * A name that matches nothing is returned unchanged, so the error that follows
37
+ * is about the tool rather than about this.
38
+ */
39
+ export function resolveToolName(name, tools) {
40
+ const names = tools.map((tool) => tool.name);
41
+ if (names.includes(name))
42
+ return name;
43
+ const matches = names.filter((candidate) => candidate.endsWith(`_${name}`));
44
+ if (matches.length === 1)
45
+ return matches[0];
46
+ if (matches.length > 1) {
47
+ throw new TrustGateError(`"${name}" is served by more than one of this application's servers ` +
48
+ `(${[...matches].sort().join(', ')}). Name the one you mean.`);
49
+ }
50
+ return name;
51
+ }
52
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,iFAAiF;AACjF,MAAM,CAAC,MAAM,KAAK,GAAG;IACpB,8DAA8D;IAC9D,WAAW,EAAE,aAAa;IAC1B,8CAA8C;IAC9C,OAAO,EAAE,UAAU;CACV,CAAA;AAGV;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACzB,wDAAwD;IACxD,eAAe,EAAE,kBAAkB;IACnC,yEAAyE;IACzE,UAAU,EAAE,aAAa;IACzB,2CAA2C;IAC3C,iBAAiB,EAAE,oBAAoB;IACvC,6DAA6D;IAC7D,MAAM,EAAE,QAAQ;CACP,CAAA;AA8CV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAoB;IACjE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACrC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IAC3E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAA;IAC3C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,cAAc,CACvB,IAAI,IAAI,6DAA6D;YACpE,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B,CAC9D,CAAA;IACF,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC"}
@@ -0,0 +1,73 @@
1
+ import type { ResolvedConfig } from './config.js';
2
+ /** Whose account an MCP server reads: the one its instance holds, or one per caller. */
3
+ export type UpstreamAccount = 'shared' | 'user';
4
+ /**
5
+ * Who has to act before a server answers a call that runs as the application.
6
+ *
7
+ * `administrator` is an instance whose shared account nobody has connected —
8
+ * no caller can connect it, because it is the account every other caller rides
9
+ * on. `end_user` is an instance that keeps an account per caller, which an
10
+ * application is not: it names the person it acts for, and the account becomes
11
+ * theirs to connect.
12
+ */
13
+ export type UpstreamBlockedBy = 'administrator' | 'end_user';
14
+ /** One MCP server the application is bound to, and what it is waiting for. */
15
+ export type KeyUpstream = {
16
+ server: string;
17
+ provider?: string;
18
+ account: UpstreamAccount;
19
+ connected: boolean;
20
+ needsReconnect: boolean;
21
+ blocked?: UpstreamBlockedBy;
22
+ };
23
+ /** One consumer the key reaches, with the address it is served on. */
24
+ export type KeyConsumer = {
25
+ slug: string;
26
+ name?: string;
27
+ /** The plane this consumer belongs to: MCP, LLM or A2A. */
28
+ type: string;
29
+ active: boolean;
30
+ /** Where it answers. Empty when the gateway publishes no host for its plane. */
31
+ url: string;
32
+ /**
33
+ * The servers behind it that read a stored account, answered for this key —
34
+ * which is the application itself.
35
+ *
36
+ * `undefined` is not "nothing to connect": it is also what a gateway that
37
+ * could not read the accounts answers, and a server carrying its own
38
+ * credential is never listed. Read `blocked`, never a length.
39
+ */
40
+ upstreams?: KeyUpstream[];
41
+ };
42
+ /** The calling key itself. The secret is never echoed. */
43
+ export type KeyInfo = {
44
+ name?: string;
45
+ /** When it retires itself. `undefined` means never. */
46
+ expiresAt?: Date;
47
+ };
48
+ /** Everything the key can say about itself. */
49
+ export type KeyIdentity = {
50
+ gateway: string;
51
+ key: KeyInfo;
52
+ consumers: KeyConsumer[];
53
+ };
54
+ /**
55
+ * Asks the key what it reaches, when it dies, and what is not connected yet.
56
+ *
57
+ * It is what lets a client be configured with one secret: the slugs were
58
+ * chosen by whoever created the consumers, and the LLM plane is on a host the
59
+ * MCP URL says nothing about, so both have to come from the gateway. The other
60
+ * two answers are the failures a client would otherwise meet at runtime — an
61
+ * expired key as a 401 mid-run, an unconnected server as a refusal on the
62
+ * first tool call — moved to where something can still be done about them.
63
+ */
64
+ export declare function whoAmI(config: ResolvedConfig, signal?: AbortSignal): Promise<KeyIdentity>;
65
+ /**
66
+ * Picks the one consumer of a plane, or explains why it cannot.
67
+ *
68
+ * A key attached to two consumers of the same type is a legitimate setup that
69
+ * this SDK cannot resolve on its own, so it names them and asks — rather than
70
+ * guessing and running an agent against the wrong surface.
71
+ */
72
+ export declare function selectConsumer(identity: KeyIdentity, plane: 'MCP' | 'LLM', configured: string | undefined, envName: string): KeyConsumer;
73
+ //# sourceMappingURL=whoami.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../src/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG,UAAU,CAAA;AAE5D,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,eAAe,CAAA;IACxB,SAAS,EAAE,OAAO,CAAA;IAClB,cAAc,EAAE,OAAO,CAAA;IACvB,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAC3B,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;IACf,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,WAAW,EAAE,CAAA;CACzB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,OAAO,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,SAAS,CAAC,EAAE,IAAI,CAAA;CAChB,CAAA;AAED,+CAA+C;AAC/C,MAAM,MAAM,WAAW,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,OAAO,CAAA;IACZ,SAAS,EAAE,WAAW,EAAE,CAAA;CACxB,CAAA;AAsBD;;;;;;;;;GASG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAwB/F;AAYD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,KAAK,GAAG,KAAK,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,MAAM,GACb,WAAW,CA+Bb"}
package/dist/whoami.js ADDED
@@ -0,0 +1,79 @@
1
+ import { PlaneUnavailableError, TrustGateError } from './errors.js';
2
+ import { requestJSON } from './http.js';
3
+ /**
4
+ * Asks the key what it reaches, when it dies, and what is not connected yet.
5
+ *
6
+ * It is what lets a client be configured with one secret: the slugs were
7
+ * chosen by whoever created the consumers, and the LLM plane is on a host the
8
+ * MCP URL says nothing about, so both have to come from the gateway. The other
9
+ * two answers are the failures a client would otherwise meet at runtime — an
10
+ * expired key as a 401 mid-run, an unconnected server as a refusal on the
11
+ * first tool call — moved to where something can still be done about them.
12
+ */
13
+ export async function whoAmI(config, signal) {
14
+ const { body } = await requestJSON(config, 'GET', '/whoami', { signal });
15
+ return {
16
+ gateway: body?.gateway ?? '',
17
+ key: {
18
+ name: body?.key?.name || undefined,
19
+ expiresAt: parseDate(body?.key?.expires_at),
20
+ },
21
+ consumers: (body?.consumers ?? []).map((consumer) => ({
22
+ slug: consumer.slug,
23
+ name: consumer.name || undefined,
24
+ type: String(consumer.type ?? '').toUpperCase(),
25
+ active: consumer.active !== false,
26
+ url: consumer.url ?? '',
27
+ upstreams: consumer.upstreams?.map((upstream) => ({
28
+ server: upstream.server ?? '',
29
+ provider: upstream.provider || undefined,
30
+ account: upstream.account === 'shared' ? 'shared' : 'user',
31
+ connected: upstream.connected === true,
32
+ needsReconnect: upstream.needs_reconnect === true,
33
+ blocked: blockedBy(upstream.blocked),
34
+ })),
35
+ })),
36
+ };
37
+ }
38
+ function blockedBy(raw) {
39
+ return raw === 'administrator' || raw === 'end_user' ? raw : undefined;
40
+ }
41
+ function parseDate(raw) {
42
+ if (!raw)
43
+ return undefined;
44
+ const at = new Date(raw);
45
+ return Number.isNaN(at.getTime()) ? undefined : at;
46
+ }
47
+ /**
48
+ * Picks the one consumer of a plane, or explains why it cannot.
49
+ *
50
+ * A key attached to two consumers of the same type is a legitimate setup that
51
+ * this SDK cannot resolve on its own, so it names them and asks — rather than
52
+ * guessing and running an agent against the wrong surface.
53
+ */
54
+ export function selectConsumer(identity, plane, configured, envName) {
55
+ const candidates = identity.consumers.filter((consumer) => consumer.type === plane);
56
+ if (configured) {
57
+ const named = candidates.find((consumer) => consumer.slug === configured);
58
+ if (named)
59
+ return named;
60
+ throw new PlaneUnavailableError(`this API key does not reach a ${plane} consumer called "${configured}"` +
61
+ (candidates.length ? `; it reaches ${candidates.map((c) => c.slug).join(', ')}` : ''));
62
+ }
63
+ if (candidates.length === 0) {
64
+ throw new PlaneUnavailableError(`this API key reaches no ${plane} consumer. ` +
65
+ 'Ask the admin who owns this application to attach one, or name it explicitly.');
66
+ }
67
+ if (candidates.length > 1) {
68
+ throw new TrustGateError(`this API key reaches several ${plane} consumers (${candidates
69
+ .map((consumer) => consumer.slug)
70
+ .join(', ')}); name the one this agent uses.`);
71
+ }
72
+ const only = candidates[0];
73
+ if (!only.url) {
74
+ throw new PlaneUnavailableError(`the gateway publishes no host for its ${plane} plane, so "${only.slug}" has no address. ` +
75
+ `Set ${envName} and a base URL for it, or ask an operator to configure that plane's domain.`);
76
+ }
77
+ return only;
78
+ }
79
+ //# sourceMappingURL=whoami.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../src/whoami.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@neuraltrust/trustgate",
3
+ "version": "0.1.0",
4
+ "description": "TrustGate SDK — governed tools and models for agents, over the MCP and LLM planes of a TrustGate gateway.",
5
+ "license": "Apache-2.0",
6
+ "author": "NeuralTrust",
7
+ "homepage": "https://docs.neuraltrust.ai/trustgate/mcp/connect",
8
+ "repository": { "type": "git", "url": "git+https://github.com/NeuralTrust/trustgate-sdk.git", "directory": "typescript" },
9
+ "bugs": { "url": "https://github.com/NeuralTrust/trustgate-sdk/issues" },
10
+ "keywords": ["mcp", "model-context-protocol", "agents", "llm", "gateway", "trustgate", "neuraltrust"],
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
15
+ "files": ["dist", "src", "README.md", "LICENSE"],
16
+ "engines": { "node": ">=18" },
17
+ "sideEffects": false,
18
+ "publishConfig": { "access": "public" },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "vitest run"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.6.0",
27
+ "vitest": "^5.0.1"
28
+ }
29
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,267 @@
1
+ import { END_USER_HEADER, type ResolvedConfig } from './config.js'
2
+ import { createConnectLink, listConnections, requireEndUser } from './connections.js'
3
+ import { ToolNotFoundError } from './errors.js'
4
+ import { adapterFor, restoreArguments, type ConversionWarning, type ToolResult } from './formats.js'
5
+ import { MCPTransport } from './mcp.js'
6
+ import {
7
+ Actor,
8
+ ToolFormat,
9
+ resolveToolName,
10
+ type ConnectLink,
11
+ type Connection,
12
+ type Endpoint,
13
+ type GatewayTool,
14
+ type JSONSchema,
15
+ type ToolCall,
16
+ } from './types.js'
17
+
18
+ export type ToolkitOptions = {
19
+ /**
20
+ * Ask the provider to hold the model to the schema. Off by default: it is
21
+ * a stricter promise bought with a lossy rewrite, and some tools cannot be
22
+ * expressed under it at all (see `warnings`).
23
+ */
24
+ strict?: boolean
25
+ }
26
+
27
+ /**
28
+ * A tool surface in one provider's dialect, with the executor that belongs to
29
+ * it.
30
+ *
31
+ * They travel together because they are two halves of one translation: what
32
+ * `tools` added on the way out, `execute` has to undo on the way back.
33
+ *
34
+ * `Tool` and `Output` are the provider's own types, named by the caller:
35
+ *
36
+ * ```ts
37
+ * const { tools, execute } = agent.toolkit<
38
+ * OpenAI.Responses.Tool,
39
+ * OpenAI.Responses.ResponseInputItem
40
+ * >(ToolFormat.OpenAIResponses)
41
+ * ```
42
+ *
43
+ * They default to `unknown`, so nothing breaks by leaving them out — but then
44
+ * "pass this straight to the provider's API" is a promise the type does not
45
+ * keep, and the caller casts at the boundary. The SDK cannot name them itself:
46
+ * it carries no dependency on any provider's package, which is what lets one
47
+ * install serve all of them.
48
+ */
49
+ export class Toolkit<Tool = unknown, Output = unknown> {
50
+ constructor(
51
+ /** Pass this straight to the provider's API. */
52
+ readonly tools: Tool[],
53
+ /** Tools whose schema could not be expressed in the requested dialect. */
54
+ readonly warnings: ConversionWarning[],
55
+ private readonly format: ToolFormat,
56
+ private readonly originals: Map<string, JSONSchema>,
57
+ private readonly transport: MCPTransport
58
+ ) {
59
+ // Bound, because the documented way to use this is to destructure it —
60
+ // `const { tools, execute } = agent.toolkit(…)` — and an unbound method
61
+ // loses the format it needs the moment it is called that way.
62
+ this.calls = this.calls.bind(this)
63
+ this.execute = this.execute.bind(this)
64
+ }
65
+
66
+ /** The calls the model asked for, read out of the provider's response. */
67
+ calls(output: unknown): ToolCall[] {
68
+ return adapterFor(this.format).extractCalls(output)
69
+ }
70
+
71
+ /**
72
+ * Runs the calls the model asked for and returns what to send back.
73
+ *
74
+ * Every call goes to the gateway, so the policy, the audit trail and the
75
+ * upstream credentials stay where they were. The caller's process only
76
+ * decides whether to make the call at all.
77
+ */
78
+ async execute(output: unknown, signal?: AbortSignal): Promise<Output[]> {
79
+ const adapter = adapterFor(this.format)
80
+ const calls = adapter.extractCalls(output)
81
+ const results: ToolResult[] = []
82
+ for (const call of calls) {
83
+ const args = restoreArguments(call.arguments, this.originals.get(call.name))
84
+ const result = await this.transport.callTool(call.name, args, signal)
85
+ results.push({ call, result })
86
+ }
87
+ return adapter.toOutputs(results) as Output[]
88
+ }
89
+ }
90
+
91
+ /**
92
+ * An application's handle on its gateway.
93
+ *
94
+ * It speaks as the application itself: one principal, its own upstream
95
+ * accounts, nothing per-user. Which handle you get is not a choice made here —
96
+ * it follows from how the consumer was configured, which is why `connect()`
97
+ * returns one of these or refuses.
98
+ */
99
+ export class Agent {
100
+ readonly actor = Actor.Application
101
+
102
+ constructor(
103
+ private readonly config: ResolvedConfig,
104
+ readonly slug: string,
105
+ private readonly transport: MCPTransport,
106
+ /** The tools this consumer serves, as the gateway named them. */
107
+ public tools: GatewayTool[],
108
+ /** Tools asked for in `requires` that the toolkit does not carry. */
109
+ readonly missing: string[],
110
+ /** The application's own upstream accounts, as of `connect()`. */
111
+ readonly connections: Connection[]
112
+ ) {}
113
+
114
+ /** URL and headers for a framework that brings its own MCP client. */
115
+ get mcp(): Endpoint {
116
+ return { url: this.transport.url, headers: this.transport.headers }
117
+ }
118
+
119
+ /**
120
+ * The same surface, translated for a provider you call directly.
121
+ *
122
+ * Name the provider's types to have them travel with it:
123
+ * `toolkit<OpenAI.Responses.Tool, OpenAI.Responses.ResponseInputItem>(…)`.
124
+ */
125
+ toolkit<Tool = unknown, Output = unknown>(
126
+ format: ToolFormat,
127
+ options: ToolkitOptions = {}
128
+ ): Toolkit<Tool, Output> {
129
+ const { tools, warnings, originals } = adapterFor(format).convert(this.tools, {
130
+ strict: options.strict ?? false,
131
+ })
132
+ return new Toolkit<Tool, Output>(
133
+ tools as Tool[],
134
+ warnings,
135
+ format,
136
+ originals,
137
+ this.transport
138
+ )
139
+ }
140
+
141
+ /**
142
+ * One tool, called directly. The escape hatch under the toolkits.
143
+ *
144
+ * The server prefix is optional here: "list_issues" reaches
145
+ * "linear_list_issues" while Linear is the only server of this application
146
+ * that serves it. The gateway put that prefix there, so a caller writing the
147
+ * name by hand should not have to.
148
+ */
149
+ async callTool(
150
+ name: string,
151
+ args: Record<string, unknown> = {},
152
+ signal?: AbortSignal
153
+ ): Promise<Record<string, unknown>> {
154
+ return this.transport.callTool(resolveToolName(name, this.tools), args, signal)
155
+ }
156
+
157
+ /**
158
+ * Re-reads the surface.
159
+ *
160
+ * An admin owns this toolkit and can change it under a running agent, so a
161
+ * long-lived process re-reads rather than trusting the list it took at
162
+ * startup.
163
+ */
164
+ async refresh(signal?: AbortSignal): Promise<GatewayTool[]> {
165
+ this.tools = await this.transport.listTools(signal)
166
+ return this.tools
167
+ }
168
+
169
+ /** What the application still owes before it can call every server. */
170
+ async refreshConnections(signal?: AbortSignal): Promise<Connection[]> {
171
+ return listConnections(this.config, this.slug, undefined, signal)
172
+ }
173
+
174
+ /**
175
+ * The same application, acting for one named person.
176
+ *
177
+ * No round trip and no second surface to read: the toolkit an admin bound
178
+ * is the application's, identical for everyone it acts for. What changes is
179
+ * one header, and with it whose upstream account the gateway reaches for.
180
+ */
181
+ forEndUser(endUser: string): EndUserAgent {
182
+ return endUserAgent(this.config, this.slug, endUser, this.transport.url, this.tools)
183
+ }
184
+ }
185
+
186
+ /**
187
+ * An application's handle for one of its own end users.
188
+ *
189
+ * The user travels in a header, so a handle is a header and nothing more —
190
+ * but the MCP endpoint's headers are fixed when a client connects, which is
191
+ * why each user needs their own transport rather than a shared one.
192
+ */
193
+ export class EndUserAgent {
194
+ readonly actor = Actor.EndUser
195
+
196
+ constructor(
197
+ private readonly config: ResolvedConfig,
198
+ readonly slug: string,
199
+ readonly endUser: string,
200
+ private readonly transport: MCPTransport,
201
+ public tools: GatewayTool[]
202
+ ) {}
203
+
204
+ get mcp(): Endpoint {
205
+ return { url: this.transport.url, headers: this.transport.headers }
206
+ }
207
+
208
+ toolkit<Tool = unknown, Output = unknown>(
209
+ format: ToolFormat,
210
+ options: ToolkitOptions = {}
211
+ ): Toolkit<Tool, Output> {
212
+ const { tools, warnings, originals } = adapterFor(format).convert(this.tools, {
213
+ strict: options.strict ?? false,
214
+ })
215
+ return new Toolkit<Tool, Output>(
216
+ tools as Tool[],
217
+ warnings,
218
+ format,
219
+ originals,
220
+ this.transport
221
+ )
222
+ }
223
+
224
+ async callTool(
225
+ name: string,
226
+ args: Record<string, unknown> = {},
227
+ signal?: AbortSignal
228
+ ): Promise<Record<string, unknown>> {
229
+ return this.transport.callTool(name, args, signal)
230
+ }
231
+
232
+ async refresh(signal?: AbortSignal): Promise<GatewayTool[]> {
233
+ this.tools = await this.transport.listTools(signal)
234
+ return this.tools
235
+ }
236
+
237
+ /** Which servers this user has connected, and which they have not. */
238
+ async connections(signal?: AbortSignal): Promise<Connection[]> {
239
+ return listConnections(this.config, this.slug, this.endUser, signal)
240
+ }
241
+
242
+ /**
243
+ * The page to put in front of this user so they can connect an account.
244
+ *
245
+ * Naming a provider narrows it to that one server; omitting it covers every
246
+ * server of the application that forwards a credential. The link expires,
247
+ * so it is minted when it is about to be shown, not cached.
248
+ */
249
+ async connectLink(provider?: string, signal?: AbortSignal): Promise<ConnectLink> {
250
+ return createConnectLink(this.config, this.slug, this.endUser, provider, signal)
251
+ }
252
+ }
253
+
254
+ /** Builds the per-user handle, with the header that names them. */
255
+ export function endUserAgent(
256
+ config: ResolvedConfig,
257
+ slug: string,
258
+ rawEndUser: string,
259
+ url: string,
260
+ tools: GatewayTool[]
261
+ ): EndUserAgent {
262
+ const endUser = requireEndUser(rawEndUser)
263
+ const transport = new MCPTransport(config, url, { [END_USER_HEADER]: endUser })
264
+ return new EndUserAgent(config, slug, endUser, transport, tools)
265
+ }
266
+
267
+ export { ToolNotFoundError }