@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 +109 -0
- package/package.json +33 -0
- package/src/caller.ts +150 -0
- package/src/index.ts +49 -0
- package/src/store.ts +217 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +10 -0
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
|
+
}
|