@datacules/agent-identity-mcp-client 0.2.1

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 ADDED
@@ -0,0 +1,109 @@
1
+ # @datacules/agent-identity-mcp-client
2
+
3
+ Outbound MCP integration for [`@datacules/agent-identity`](../../core). Consumes external MCP servers as `CredentialStore` implementations, allowing the credential router to pull credentials **from** any MCP-speaking secrets server — such as another `@datacules/agent-identity-mcp` instance, a Vault MCP server, a 1Password MCP server, or a custom server.
4
+
5
+ ## Exports
6
+
7
+ | Export | Description |
8
+ |--------|-------------|
9
+ | `McpCredentialStore` | `CredentialStore` impl — fetches via MCP `list_credentials` tool |
10
+ | `McpToolCaller` | Direct tool caller — `resolveCredential`, `health`, arbitrary tools |
11
+
12
+ Both classes support `http` (SSE to a running server) and `stdio` (spawns a process) transports.
13
+
14
+ ## Usage — McpCredentialStore
15
+
16
+ Drop it into any `CredentialRouter` with no other changes:
17
+
18
+ ```typescript
19
+ import { McpCredentialStore } from '@datacules/agent-identity-mcp-client';
20
+ import { createRouterFromStore } from '@datacules/agent-identity';
21
+
22
+ // HTTP transport (connect to a running agent-identity-mcp server)
23
+ const store = new McpCredentialStore({
24
+ transport: 'http',
25
+ serverUrl: 'http://vault-mcp.internal:3002',
26
+ authToken: process.env.MCP_AUTH_TOKEN, // optional
27
+ cacheTtlMs: 30_000, // optional, default 60s
28
+ });
29
+
30
+ const router = createRouterFromStore(store, rules, logger);
31
+
32
+ // Credential resolution is now backed by the remote MCP server
33
+ const resolved = router.resolve(ctx);
34
+
35
+ // Disconnect on shutdown
36
+ process.on('SIGTERM', () => store.disconnect());
37
+ ```
38
+
39
+ ```typescript
40
+ // stdio transport (spawn a local server process)
41
+ const store = new McpCredentialStore({
42
+ transport: 'stdio',
43
+ command: 'npx',
44
+ args: ['@datacules/agent-identity-mcp'],
45
+ env: {
46
+ AGENT_IDENTITY_CREDENTIALS: process.env.AGENT_IDENTITY_CREDENTIALS!,
47
+ AGENT_IDENTITY_RULES: process.env.AGENT_IDENTITY_RULES!,
48
+ },
49
+ });
50
+ ```
51
+
52
+ ## Usage — McpToolCaller
53
+
54
+ For when you want to call the MCP server directly, without a local router:
55
+
56
+ ```typescript
57
+ import { McpToolCaller } from '@datacules/agent-identity-mcp-client';
58
+
59
+ const caller = new McpToolCaller({
60
+ transport: 'http',
61
+ serverUrl: 'http://localhost:3002',
62
+ });
63
+
64
+ // Typed helpers
65
+ const result = await caller.resolveCredential({
66
+ userId: 'user-1', resourceId: 'kb-1', resourceKind: 'personal',
67
+ provider: 'anthropic', model: 'claude-sonnet-4-20250514',
68
+ action: 'read', traceId: 'trace-001',
69
+ });
70
+ console.log(result.credentialId, result.resolvedFor);
71
+
72
+ const health = await caller.health();
73
+ console.log(health.credentialsLoaded, health.rulesLoaded);
74
+
75
+ // Arbitrary tool call
76
+ const rules = await caller.callTool('list_rules', {});
77
+
78
+ await caller.disconnect();
79
+ ```
80
+
81
+ ## Full MCP integration picture
82
+
83
+ ```
84
+ ┌──────────────────────────────────────────┐
85
+ │ MCP Client │
86
+ │ (Claude Desktop / Claude Code / │
87
+ │ Cursor / custom agent) │
88
+ └──────────────────────────────────────────┘
89
+ │ MCP tools (resolve_credential etc.)
90
+ ▼ INBOUND
91
+ ┌──────────────────────────────────────────┐
92
+ │ @datacules/agent-identity-mcp │
93
+ │ (MCP Server — stdio or HTTP+SSE) │
94
+ └──────────────────────────────────────────┘
95
+ │ CredentialStore interface
96
+
97
+ ┌──────────────────────────────────────────┐
98
+ │ @datacules/agent-identity-mcp-client │
99
+ │ McpCredentialStore │ OUTBOUND
100
+ │ (fetches from external MCP servers) │
101
+ └──────────────────────────────────────────┘
102
+ │ MCP tools (list_credentials)
103
+
104
+ ┌──────────────────────────────────────────┐
105
+ │ External MCP Credential Server │
106
+ │ (Vault MCP / 1Password MCP / │
107
+ │ custom secrets server) │
108
+ └──────────────────────────────────────────┘
109
+ ```
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@datacules/agent-identity-mcp-client",
3
+ "version": "0.2.1",
4
+ "private": false,
5
+ "description": "MCP client adapter for @datacules/agent-identity — consume external MCP servers as CredentialStores",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "type-check": "tsc --noEmit"
12
+ },
13
+ "peerDependencies": {
14
+ "@datacules/agent-identity": "^0.1.0"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.10.0"
18
+ },
19
+ "devDependencies": {
20
+ "@datacules/agent-identity": "*",
21
+ "typescript": "^5"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "agent-identity",
30
+ "credential",
31
+ "datacules"
32
+ ]
33
+ }
package/src/caller.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * McpToolCaller — utility for calling arbitrary tools on a connected
3
+ * agent-identity MCP server from application code.
4
+ *
5
+ * While McpCredentialStore handles the CredentialStore contract,
6
+ * McpToolCaller lets you call any tool (resolve_credential,
7
+ * resolve_migration_credential, health, etc.) directly — useful
8
+ * when you want the MCP server to perform the resolution and return
9
+ * the result without going through a local CredentialRouter.
10
+ *
11
+ * Example:
12
+ * const caller = new McpToolCaller({ transport: 'http', serverUrl: 'http://localhost:3002' });
13
+ * const result = await caller.resolveCredential({ userId: 'u1', ... });
14
+ * await caller.disconnect();
15
+ */
16
+
17
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
19
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
20
+ import type { McpCredentialStoreOptions } from './store.js';
21
+
22
+ export type McpToolCallerOptions = McpCredentialStoreOptions;
23
+
24
+ export interface ResolvedCredentialResult {
25
+ ok: boolean;
26
+ credentialId: string;
27
+ kind: 'fixed' | 'user-delegated';
28
+ resolvedFor: string;
29
+ }
30
+
31
+ export interface ResolvedMigrationResult {
32
+ ok: boolean;
33
+ migrationId: string;
34
+ source: ResolvedCredentialResult;
35
+ target: ResolvedCredentialResult;
36
+ expiresAt: string | null;
37
+ }
38
+
39
+ export interface HealthResult {
40
+ status: 'ok' | 'error';
41
+ credentialsLoaded: number;
42
+ rulesLoaded: number;
43
+ timestamp: string;
44
+ }
45
+
46
+ /**
47
+ * Thin wrapper around the MCP SDK Client for calling agent-identity
48
+ * MCP tools directly. Connection is lazy and cached after first call.
49
+ */
50
+ export class McpToolCaller {
51
+ private client: Client | null = null;
52
+ private connectPromise: Promise<void> | null = null;
53
+ private readonly options: McpToolCallerOptions;
54
+
55
+ constructor(options: McpToolCallerOptions) {
56
+ this.options = options;
57
+ }
58
+
59
+ // ── High-level helpers ─────────────────────────────────────────────────────
60
+
61
+ /** Resolve a credential via the remote MCP server */
62
+ async resolveCredential(
63
+ ctx: Record<string, unknown>
64
+ ): Promise<ResolvedCredentialResult> {
65
+ return this.callTool<ResolvedCredentialResult>('resolve_credential', ctx);
66
+ }
67
+
68
+ /** Resolve a migration credential pair via the remote MCP server */
69
+ async resolveMigrationCredential(
70
+ ctx: Record<string, unknown>
71
+ ): Promise<ResolvedMigrationResult> {
72
+ return this.callTool<ResolvedMigrationResult>('resolve_migration_credential', ctx);
73
+ }
74
+
75
+ /** Check the health of the remote agent-identity MCP server */
76
+ async health(): Promise<HealthResult> {
77
+ return this.callTool<HealthResult>('health', {});
78
+ }
79
+
80
+ // ── Generic tool call ────────────────────────────────────────────────────────
81
+
82
+ async callTool<T = unknown>(
83
+ toolName: string,
84
+ args: Record<string, unknown>
85
+ ): Promise<T> {
86
+ await this.ensureConnected();
87
+
88
+ const result = await this.client!.callTool({ name: toolName, arguments: args });
89
+
90
+ const text = (result.content as Array<{ type: string; text: string }>)
91
+ .filter((c) => c.type === 'text')
92
+ .map((c) => c.text)
93
+ .join('');
94
+
95
+ let parsed: T;
96
+ try {
97
+ parsed = JSON.parse(text) as T;
98
+ } catch {
99
+ throw new Error(
100
+ `[McpToolCaller] Tool "${toolName}" returned non-JSON: ${text.slice(0, 200)}`
101
+ );
102
+ }
103
+
104
+ if ((parsed as any)?.error) {
105
+ throw new Error(`[McpToolCaller] Tool "${toolName}" error: ${(parsed as any).error}`);
106
+ }
107
+
108
+ return parsed;
109
+ }
110
+
111
+ /** Disconnect from the remote MCP server */
112
+ async disconnect(): Promise<void> {
113
+ if (this.client) {
114
+ await this.client.close();
115
+ this.client = null;
116
+ }
117
+ }
118
+
119
+ // ── Connection lifecycle ─────────────────────────────────────────────────
120
+
121
+ private async ensureConnected(): Promise<void> {
122
+ if (this.client) return;
123
+ if (!this.connectPromise) this.connectPromise = this._connect();
124
+ await this.connectPromise;
125
+ this.connectPromise = null;
126
+ }
127
+
128
+ private async _connect(): Promise<void> {
129
+ const opts = this.options;
130
+ this.client = new Client(
131
+ { name: opts.clientName ?? 'agent-identity-mcp-caller', version: opts.clientVersion ?? '0.1.0' },
132
+ { capabilities: {} }
133
+ );
134
+
135
+ if (opts.transport === 'http') {
136
+ const sseUrl = new URL('/sse', opts.serverUrl);
137
+ const headers: Record<string, string> = {};
138
+ if (opts.authToken) headers['Authorization'] = `Bearer ${opts.authToken}`;
139
+ await this.client.connect(new SSEClientTransport(sseUrl, { headers }));
140
+ } else {
141
+ await this.client.connect(
142
+ new StdioClientTransport({
143
+ command: opts.command,
144
+ args: opts.args ?? [],
145
+ env: opts.env,
146
+ })
147
+ );
148
+ }
149
+ }
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @datacules/agent-identity-mcp-client
3
+ *
4
+ * Consumes external MCP servers as CredentialStores — the outbound direction
5
+ * of the agent-identity MCP integration.
6
+ *
7
+ * Exports:
8
+ * McpCredentialStore — CredentialStore impl that fetches credentials from
9
+ * any MCP server exposing a list_credentials tool
10
+ * McpToolCaller — thin client for calling any agent-identity MCP tool
11
+ * directly (resolve_credential, health, etc.)
12
+ *
13
+ * Both classes support two transports:
14
+ * http — SSE to a running HTTP+SSE agent-identity-mcp server
15
+ * stdio — spawns an MCP server process and communicates over stdio
16
+ *
17
+ * Example — plug McpCredentialStore into a CredentialRouter:
18
+ *
19
+ * import { McpCredentialStore } from '@datacules/agent-identity-mcp-client';
20
+ * import { createRouterFromStore } from '@datacules/agent-identity';
21
+ *
22
+ * const store = new McpCredentialStore({
23
+ * transport: 'http',
24
+ * serverUrl: 'http://vault-mcp.internal:3002',
25
+ * authToken: process.env.MCP_AUTH_TOKEN,
26
+ * });
27
+ *
28
+ * const router = createRouterFromStore(store, rules, logger);
29
+ * const resolved = await router.resolveAsync(ctx); // async path via store
30
+ *
31
+ * // Clean up when done
32
+ * process.on('SIGTERM', () => store.disconnect());
33
+ *
34
+ * Example — call the MCP server directly (no local router):
35
+ *
36
+ * import { McpToolCaller } from '@datacules/agent-identity-mcp-client';
37
+ *
38
+ * const caller = new McpToolCaller({
39
+ * transport: 'http',
40
+ * serverUrl: 'http://localhost:3002',
41
+ * });
42
+ * const result = await caller.resolveCredential({ userId: 'u1', ... });
43
+ * await caller.disconnect();
44
+ */
45
+
46
+ export { McpCredentialStore } from './store.js';
47
+ export type { McpCredentialStoreOptions, McpCredentialStoreHttpOptions, McpCredentialStoreStdioOptions } from './store.js';
48
+ export { McpToolCaller } from './caller.js';
49
+ export type { McpToolCallerOptions, ResolvedCredentialResult, ResolvedMigrationResult, HealthResult } from './caller.js';
package/src/store.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * McpCredentialStore — CredentialStore implementation that fetches
3
+ * credentials from an external MCP server.
4
+ *
5
+ * Implements the full CredentialStore interface from @datacules/agent-identity
6
+ * so it can be dropped into any CredentialRouter without any other changes:
7
+ *
8
+ * import { McpCredentialStore } from '@datacules/agent-identity-mcp-client';
9
+ * import { createRouterFromStore } from '@datacules/agent-identity';
10
+ *
11
+ * const store = new McpCredentialStore({
12
+ * serverUrl: 'http://localhost:3002',
13
+ * authToken: process.env.MCP_AUTH_TOKEN,
14
+ * });
15
+ * const router = createRouterFromStore(store, rules, logger);
16
+ *
17
+ * The MCP server this client connects to MUST expose a `list_credentials`
18
+ * tool (i.e. another @datacules/agent-identity-mcp instance, a Vault MCP
19
+ * server, a 1Password MCP server, or any custom server following the same
20
+ * tool contract).
21
+ *
22
+ * Transport:
23
+ * - For a remote HTTP+SSE server: provide serverUrl
24
+ * - For an in-process stdio server (test / monorepo): provide serverProcess
25
+ */
26
+
27
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
28
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
29
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
30
+ import type { Credential, CredentialStore } from '@datacules/agent-identity';
31
+
32
+ // ─── Options ───────────────────────────────────────────────────────────────────
33
+
34
+ export interface McpCredentialStoreHttpOptions {
35
+ transport: 'http';
36
+ /**
37
+ * Base URL of the remote agent-identity MCP server.
38
+ * The SSE endpoint is expected at GET <serverUrl>/sse
39
+ * and messages at POST <serverUrl>/messages.
40
+ */
41
+ serverUrl: string;
42
+ /** Bearer token if the remote server requires auth */
43
+ authToken?: string;
44
+ /** Client name sent during MCP handshake (default: 'agent-identity-mcp-client') */
45
+ clientName?: string;
46
+ /** Client version (default: '0.1.0') */
47
+ clientVersion?: string;
48
+ /** TTL of the in-memory credential cache in ms (default: 60_000) */
49
+ cacheTtlMs?: number;
50
+ }
51
+
52
+ export interface McpCredentialStoreStdioOptions {
53
+ transport: 'stdio';
54
+ /** Command to spawn the MCP server process */
55
+ command: string;
56
+ /** Arguments passed to the spawned process */
57
+ args?: string[];
58
+ /** Environment variables for the spawned process */
59
+ env?: Record<string, string>;
60
+ clientName?: string;
61
+ clientVersion?: string;
62
+ cacheTtlMs?: number;
63
+ }
64
+
65
+ export type McpCredentialStoreOptions =
66
+ | McpCredentialStoreHttpOptions
67
+ | McpCredentialStoreStdioOptions;
68
+
69
+ // ─── Cache entry ────────────────────────────────────────────────────────────
70
+
71
+ interface CacheEntry {
72
+ credentials: Credential[];
73
+ expiresAt: number;
74
+ }
75
+
76
+ // ─── McpCredentialStore ─────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * CredentialStore implementation that pulls credentials from an external
80
+ * MCP server by calling its `list_credentials` tool.
81
+ *
82
+ * Credentials are cached in-memory for `cacheTtlMs` (default 60s) to avoid
83
+ * a round-trip on every router.resolve() call. Call invalidateCache() to
84
+ * force a fresh fetch on the next operation.
85
+ *
86
+ * The MCP client connection is lazy — the first store operation connects
87
+ * and caches the connection. Call disconnect() when the store is no longer
88
+ * needed (e.g. on process shutdown).
89
+ */
90
+ export class McpCredentialStore implements CredentialStore {
91
+ private client: Client | null = null;
92
+ private cache: CacheEntry | null = null;
93
+ private readonly cacheTtlMs: number;
94
+ private readonly options: McpCredentialStoreOptions;
95
+ private connectPromise: Promise<void> | null = null;
96
+
97
+ constructor(options: McpCredentialStoreOptions) {
98
+ this.options = options;
99
+ this.cacheTtlMs = options.cacheTtlMs ?? 60_000;
100
+ }
101
+
102
+ // ── Public CredentialStore interface ────────────────────────────────────────
103
+
104
+ async findByRef(ref: string): Promise<Credential | null> {
105
+ const all = await this.listActive();
106
+ return all.find((c) => c.ref === ref && c.status === 'active') ?? null;
107
+ }
108
+
109
+ async listActive(): Promise<Credential[]> {
110
+ const cached = this.getFromCache();
111
+ if (cached) return cached.filter((c) => c.status === 'active');
112
+ const fresh = await this.fetchFromServer();
113
+ return fresh.filter((c) => c.status === 'active');
114
+ }
115
+
116
+ async listByKind(kind: Credential['kind']): Promise<Credential[]> {
117
+ const all = await this.listActive();
118
+ return all.filter((c) => c.kind === kind);
119
+ }
120
+
121
+ // ── Cache management ──────────────────────────────────────────────────────
122
+
123
+ /** Force the next store operation to fetch fresh credentials from the server */
124
+ invalidateCache(): void {
125
+ this.cache = null;
126
+ }
127
+
128
+ private getFromCache(): Credential[] | null {
129
+ if (!this.cache) return null;
130
+ if (Date.now() > this.cache.expiresAt) { this.cache = null; return null; }
131
+ return this.cache.credentials;
132
+ }
133
+
134
+ private setCache(credentials: Credential[]): void {
135
+ this.cache = { credentials, expiresAt: Date.now() + this.cacheTtlMs };
136
+ }
137
+
138
+ // ── MCP client lifecycle ─────────────────────────────────────────────────
139
+
140
+ private async ensureConnected(): Promise<void> {
141
+ if (this.client) return;
142
+ // Serialize concurrent callers so we only connect once
143
+ if (!this.connectPromise) this.connectPromise = this._connect();
144
+ await this.connectPromise;
145
+ this.connectPromise = null;
146
+ }
147
+
148
+ private async _connect(): Promise<void> {
149
+ const opts = this.options;
150
+ const clientName = opts.clientName ?? 'agent-identity-mcp-client';
151
+ const clientVersion = opts.clientVersion ?? '0.1.0';
152
+
153
+ this.client = new Client(
154
+ { name: clientName, version: clientVersion },
155
+ { capabilities: {} }
156
+ );
157
+
158
+ if (opts.transport === 'http') {
159
+ const sseUrl = new URL('/sse', opts.serverUrl);
160
+ const headers: Record<string, string> = {};
161
+ if (opts.authToken) headers['Authorization'] = `Bearer ${opts.authToken}`;
162
+
163
+ const transport = new SSEClientTransport(sseUrl, { headers });
164
+ await this.client.connect(transport);
165
+ } else {
166
+ const transport = new StdioClientTransport({
167
+ command: opts.command,
168
+ args: opts.args ?? [],
169
+ env: opts.env,
170
+ });
171
+ await this.client.connect(transport);
172
+ }
173
+ }
174
+
175
+ /** Disconnect the MCP client and clear the cache. Call on process shutdown. */
176
+ async disconnect(): Promise<void> {
177
+ this.invalidateCache();
178
+ if (this.client) {
179
+ await this.client.close();
180
+ this.client = null;
181
+ }
182
+ }
183
+
184
+ // ── Remote fetch ───────────────────────────────────────────────────────────
185
+
186
+ private async fetchFromServer(): Promise<Credential[]> {
187
+ await this.ensureConnected();
188
+
189
+ const result = await this.client!.callTool({
190
+ name: 'list_credentials',
191
+ arguments: {},
192
+ });
193
+
194
+ const text = (result.content as Array<{ type: string; text: string }>)
195
+ .filter((c) => c.type === 'text')
196
+ .map((c) => c.text)
197
+ .join('');
198
+
199
+ let parsed: { credentials?: Credential[] };
200
+ try {
201
+ parsed = JSON.parse(text);
202
+ } catch {
203
+ throw new Error(
204
+ `[McpCredentialStore] MCP server returned non-JSON response from list_credentials: ${text.slice(0, 200)}`
205
+ );
206
+ }
207
+
208
+ if (!Array.isArray(parsed.credentials)) {
209
+ throw new Error(
210
+ '[McpCredentialStore] MCP server list_credentials response missing credentials array'
211
+ );
212
+ }
213
+
214
+ this.setCache(parsed.credentials);
215
+ return parsed.credentials;
216
+ }
217
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declarationOnly": false,
5
+ "outDir": "dist/esm",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler"
8
+ }
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist/types",
6
+ "declaration": true,
7
+ "declarationOnly": true
8
+ },
9
+ "include": ["src"]
10
+ }