@ferrvault/mcp 10.12.0 → 11.0.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/api.d.ts +19 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +32 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +14 -0
- package/dist/register.js +12 -12
- package/dist/results.d.ts +13 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +33 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +40 -0
- package/dist/secret.d.ts +18 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +4 -0
- package/dist/tools/secret-copy.d.ts +3 -0
- package/dist/tools/secret-copy.d.ts.map +1 -0
- package/dist/tools/secret-copy.js +33 -0
- package/dist/tools/secret-requests.d.ts +3 -0
- package/dist/tools/secret-requests.d.ts.map +1 -0
- package/dist/tools/secret-requests.js +58 -0
- package/dist/tools/secret-writes.d.ts +3 -0
- package/dist/tools/secret-writes.d.ts.map +1 -0
- package/dist/tools/secret-writes.js +52 -0
- package/dist/tools/secrets.d.ts +1 -1
- package/dist/tools/secrets.d.ts.map +1 -1
- package/dist/tools/secrets.js +21 -29
- package/dist/tools/service-tokens.d.ts +3 -0
- package/dist/tools/service-tokens.d.ts.map +1 -0
- package/dist/tools/service-tokens.js +82 -0
- package/dist/tools/vaults.d.ts +1 -1
- package/dist/tools/vaults.d.ts.map +1 -1
- package/dist/tools/vaults.js +21 -50
- package/dist/upsert.d.ts +9 -0
- package/dist/upsert.d.ts.map +1 -0
- package/dist/upsert.js +28 -0
- package/package.json +5 -5
- package/LICENSE +0 -373
- package/dist/resources/vaults.d.ts +0 -3
- package/dist/resources/vaults.d.ts.map +0 -1
- package/dist/resources/vaults.js +0 -16
- package/dist/tools/__tests__/mutations.test.d.ts +0 -2
- package/dist/tools/__tests__/mutations.test.d.ts.map +0 -1
- package/dist/tools/__tests__/mutations.test.js +0 -120
- package/dist/tools/audit.d.ts +0 -3
- package/dist/tools/audit.d.ts.map +0 -1
- package/dist/tools/audit.js +0 -39
- package/dist/tools/secret-mutations.d.ts +0 -3
- package/dist/tools/secret-mutations.d.ts.map +0 -1
- package/dist/tools/secret-mutations.js +0 -86
- package/dist/tools/vault-details.d.ts +0 -13
- package/dist/tools/vault-details.d.ts.map +0 -1
- package/dist/tools/vault-details.js +0 -18
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
process.env.FERRLABS_API_TOKEN = 'test-token';
|
|
3
|
-
const handlers = new Map();
|
|
4
|
-
const schemas = new Map();
|
|
5
|
-
const mockServer = {
|
|
6
|
-
tool: vi.fn((name, _desc, schema, handler) => {
|
|
7
|
-
handlers.set(name, handler);
|
|
8
|
-
schemas.set(name, schema);
|
|
9
|
-
}),
|
|
10
|
-
};
|
|
11
|
-
const mockFetch = vi.fn();
|
|
12
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
13
|
-
function ok(body = { id: 's1' }) {
|
|
14
|
-
return {
|
|
15
|
-
ok: true,
|
|
16
|
-
status: 200,
|
|
17
|
-
text: () => Promise.resolve(JSON.stringify(body)),
|
|
18
|
-
json: () => Promise.resolve(body),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function lastCall() {
|
|
22
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
-
return {
|
|
24
|
-
url: String(url),
|
|
25
|
-
method: init.method ?? 'GET',
|
|
26
|
-
body: init.body ? JSON.parse(init.body) : undefined,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const VAULT_BASE = 'https://api.ferrlabs.com/orgs/acme/projects/web/vaults/v1';
|
|
30
|
-
describe('ferrvault write tools', () => {
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
vi.clearAllMocks();
|
|
33
|
-
handlers.clear();
|
|
34
|
-
schemas.clear();
|
|
35
|
-
mockFetch.mockResolvedValue(ok());
|
|
36
|
-
const { registerSecretMutationTools } = await import('../secret-mutations.js');
|
|
37
|
-
const { registerVaultMutationTools } = await import('../vaults.js');
|
|
38
|
-
registerSecretMutationTools(mockServer);
|
|
39
|
-
registerVaultMutationTools(mockServer);
|
|
40
|
-
});
|
|
41
|
-
it('create_secret posts to the vault secrets collection on the FerrLabs API', async () => {
|
|
42
|
-
await handlers.get('create_secret')({
|
|
43
|
-
org_slug: 'acme',
|
|
44
|
-
project_slug: 'web',
|
|
45
|
-
vault_id: 'v1',
|
|
46
|
-
name: 'STRIPE_API_KEY',
|
|
47
|
-
value: 's3cret',
|
|
48
|
-
});
|
|
49
|
-
const call = lastCall();
|
|
50
|
-
expect(call.url).toBe(`${VAULT_BASE}/secrets`);
|
|
51
|
-
expect(call.method).toBe('POST');
|
|
52
|
-
expect(call.body).toEqual({ name: 'STRIPE_API_KEY', value: 's3cret', description: null });
|
|
53
|
-
});
|
|
54
|
-
it('update_secret patches only the fields that were passed', async () => {
|
|
55
|
-
await handlers.get('update_secret')({
|
|
56
|
-
org_slug: 'acme',
|
|
57
|
-
project_slug: 'web',
|
|
58
|
-
vault_id: 'v1',
|
|
59
|
-
secret_id: 'sec1',
|
|
60
|
-
description: 'rotated by hand',
|
|
61
|
-
});
|
|
62
|
-
const call = lastCall();
|
|
63
|
-
expect(call.url).toBe(`${VAULT_BASE}/secrets/sec1`);
|
|
64
|
-
expect(call.method).toBe('PATCH');
|
|
65
|
-
expect(call.body).toEqual({ description: 'rotated by hand' });
|
|
66
|
-
});
|
|
67
|
-
it('rotate_secret posts to the rotate sub-resource, not the secret itself', async () => {
|
|
68
|
-
await handlers.get('rotate_secret')({
|
|
69
|
-
org_slug: 'acme',
|
|
70
|
-
project_slug: 'web',
|
|
71
|
-
vault_id: 'v1',
|
|
72
|
-
secret_id: 'sec1',
|
|
73
|
-
value: 'new-value',
|
|
74
|
-
});
|
|
75
|
-
const call = lastCall();
|
|
76
|
-
expect(call.url).toBe(`${VAULT_BASE}/secrets/sec1/rotate`);
|
|
77
|
-
expect(call.method).toBe('POST');
|
|
78
|
-
expect(call.body).toEqual({ value: 'new-value' });
|
|
79
|
-
});
|
|
80
|
-
it('delete_secret uses DELETE and names the id it removed', async () => {
|
|
81
|
-
const result = await handlers.get('delete_secret')({
|
|
82
|
-
org_slug: 'acme',
|
|
83
|
-
project_slug: 'web',
|
|
84
|
-
vault_id: 'v1',
|
|
85
|
-
secret_id: 'sec1',
|
|
86
|
-
});
|
|
87
|
-
const call = lastCall();
|
|
88
|
-
expect(call.url).toBe(`${VAULT_BASE}/secrets/sec1`);
|
|
89
|
-
expect(call.method).toBe('DELETE');
|
|
90
|
-
expect(result.content[0].text).toContain('sec1');
|
|
91
|
-
});
|
|
92
|
-
it('delete_vault targets the vault itself rather than a secret under it', async () => {
|
|
93
|
-
const result = await handlers.get('delete_vault')({
|
|
94
|
-
org_slug: 'acme',
|
|
95
|
-
project_slug: 'web',
|
|
96
|
-
vault_id: 'v1',
|
|
97
|
-
});
|
|
98
|
-
const call = lastCall();
|
|
99
|
-
expect(call.url).toBe(VAULT_BASE);
|
|
100
|
-
expect(call.method).toBe('DELETE');
|
|
101
|
-
expect(result.content[0].text).toContain('v1');
|
|
102
|
-
});
|
|
103
|
-
it('escapes path segments so a slug cannot climb the path', async () => {
|
|
104
|
-
await handlers.get('delete_secret')({
|
|
105
|
-
org_slug: 'acme',
|
|
106
|
-
project_slug: 'web',
|
|
107
|
-
vault_id: 'v1',
|
|
108
|
-
secret_id: '../../../orgs',
|
|
109
|
-
});
|
|
110
|
-
const call = lastCall();
|
|
111
|
-
expect(call.url).toBe(`${VAULT_BASE}/secrets/..%2F..%2F..%2Forgs`);
|
|
112
|
-
expect(call.url).not.toContain('/../');
|
|
113
|
-
});
|
|
114
|
-
it('rejects a secret name that is not SCREAMING_SNAKE_CASE', () => {
|
|
115
|
-
const name = schemas.get('create_secret').name;
|
|
116
|
-
expect(name.safeParse('STRIPE_API_KEY').success).toBe(true);
|
|
117
|
-
expect(name.safeParse('stripe_api_key').success).toBe(false);
|
|
118
|
-
expect(name.safeParse('Stripe-Api-Key').success).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
});
|
package/dist/tools/audit.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/tools/audit.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAqBtF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,QAqDxD"}
|
package/dist/tools/audit.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { apiRequest, getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
|
-
export function registerVaultAuditTools(server) {
|
|
4
|
-
server.tool('get_vault_audit_log', 'Recent audit-log entries for a FerrVault vault — who read/wrote/rotated what, when.', {
|
|
5
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
6
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
7
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
8
|
-
limit: z
|
|
9
|
-
.number()
|
|
10
|
-
.int()
|
|
11
|
-
.min(1)
|
|
12
|
-
.max(200)
|
|
13
|
-
.optional()
|
|
14
|
-
.describe('Max entries to return (default 50).'),
|
|
15
|
-
}, async ({ org_slug, project_slug, vault_id, limit }) => {
|
|
16
|
-
const token = await getToken();
|
|
17
|
-
const qs = limit !== undefined ? `?limit=${limit}` : '';
|
|
18
|
-
const entries = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/projects/${encodeURIComponent(project_slug)}/vaults/${encodeURIComponent(vault_id)}/audit-log${qs}`, { token });
|
|
19
|
-
return {
|
|
20
|
-
content: [
|
|
21
|
-
{
|
|
22
|
-
type: 'text',
|
|
23
|
-
text: toToolText(entries, { narrowWith: 'Pass a smaller limit.' }),
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
server.tool('list_secret_requests', 'List pending "missing secret" requests filed by operators/SDKs when a name lookup misses. Useful to spot secrets that need to be provisioned.', {
|
|
29
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
30
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
31
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
32
|
-
}, async ({ org_slug, project_slug, vault_id }) => {
|
|
33
|
-
const token = await getToken();
|
|
34
|
-
const requests = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/projects/${encodeURIComponent(project_slug)}/vaults/${encodeURIComponent(vault_id)}/secret-requests`, { token });
|
|
35
|
-
return {
|
|
36
|
-
content: [{ type: 'text', text: toToolText(requests) }],
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"secret-mutations.d.ts","sourceRoot":"","sources":["../../src/tools/secret-mutations.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAetF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,QA2H5D"}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { apiRequest, getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
|
-
function vaultBase(org_slug, project_slug, vault_id) {
|
|
4
|
-
return `/orgs/${encodeURIComponent(org_slug)}/projects/${encodeURIComponent(project_slug)}/vaults/${encodeURIComponent(vault_id)}`;
|
|
5
|
-
}
|
|
6
|
-
export function registerSecretMutationTools(server) {
|
|
7
|
-
server.tool('create_secret', 'Create a new secret inside a vault. The value is encrypted server-side under the org KEK (envelope encryption) — it never lands in clear in the DB.', {
|
|
8
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
9
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
10
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
11
|
-
name: z
|
|
12
|
-
.string()
|
|
13
|
-
.min(1)
|
|
14
|
-
.max(200)
|
|
15
|
-
.regex(/^[A-Z][A-Z0-9_]*$/, 'name must be SCREAMING_SNAKE_CASE')
|
|
16
|
-
.describe('Secret name (e.g. STRIPE_API_KEY)'),
|
|
17
|
-
value: z.string().min(1).describe('Plaintext value — encrypted at rest by the server.'),
|
|
18
|
-
description: z.string().max(500).optional().describe('Optional human-readable note.'),
|
|
19
|
-
}, async ({ org_slug, project_slug, vault_id, name, value, description }) => {
|
|
20
|
-
const token = await getToken();
|
|
21
|
-
const secret = await apiRequest(`${vaultBase(org_slug, project_slug, vault_id)}/secrets`, {
|
|
22
|
-
token,
|
|
23
|
-
method: 'POST',
|
|
24
|
-
body: { name, value, description: description ?? null },
|
|
25
|
-
});
|
|
26
|
-
return {
|
|
27
|
-
content: [{ type: 'text', text: toToolText(secret) }],
|
|
28
|
-
};
|
|
29
|
-
});
|
|
30
|
-
server.tool('update_secret', 'Update a secret. Pass `value` to rotate it in place (preserves name + id); pass `description` to retitle it. Either or both may be omitted; nothing is touched unless you pass it.', {
|
|
31
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
32
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
33
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
34
|
-
secret_id: z.string().min(1).describe('Secret id'),
|
|
35
|
-
value: z.string().min(1).optional().describe('New plaintext value.'),
|
|
36
|
-
description: z.string().max(500).nullable().optional(),
|
|
37
|
-
}, async ({ org_slug, project_slug, vault_id, secret_id, ...patch }) => {
|
|
38
|
-
const token = await getToken();
|
|
39
|
-
const body = {};
|
|
40
|
-
for (const [k, v] of Object.entries(patch)) {
|
|
41
|
-
if (v !== undefined)
|
|
42
|
-
body[k] = v;
|
|
43
|
-
}
|
|
44
|
-
const secret = await apiRequest(`${vaultBase(org_slug, project_slug, vault_id)}/secrets/${encodeURIComponent(secret_id)}`, { token, method: 'PATCH', body });
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: 'text', text: toToolText(secret) }],
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
server.tool('delete_secret', 'Permanently delete a secret. Irreversible at the API level — recovery only via the soft-delete window on the FerrVault dashboard.', {
|
|
50
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
51
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
52
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
53
|
-
secret_id: z.string().min(1).describe('Secret id'),
|
|
54
|
-
}, async ({ org_slug, project_slug, vault_id, secret_id }) => {
|
|
55
|
-
const token = await getToken();
|
|
56
|
-
await apiRequest(`${vaultBase(org_slug, project_slug, vault_id)}/secrets/${encodeURIComponent(secret_id)}`, { token, method: 'DELETE' });
|
|
57
|
-
return {
|
|
58
|
-
content: [{ type: 'text', text: `Secret ${secret_id} deleted.` }],
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
server.tool('rotate_secret', 'Rotate a secret in-place — fans the new value through any FerrVault K8s operator deployments that mount it via SecretRef, so consumers pick up the new value on next sync without an app restart.', {
|
|
62
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
63
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
64
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
65
|
-
secret_id: z.string().min(1).describe('Secret id'),
|
|
66
|
-
value: z.string().min(1).describe('New plaintext value (will replace the current).'),
|
|
67
|
-
}, async ({ org_slug, project_slug, vault_id, secret_id, value }) => {
|
|
68
|
-
const token = await getToken();
|
|
69
|
-
const secret = await apiRequest(`${vaultBase(org_slug, project_slug, vault_id)}/secrets/${encodeURIComponent(secret_id)}/rotate`, { token, method: 'POST', body: { value } });
|
|
70
|
-
return {
|
|
71
|
-
content: [{ type: 'text', text: toToolText(secret) }],
|
|
72
|
-
};
|
|
73
|
-
});
|
|
74
|
-
server.tool('archive_secret_request', 'Archive a "missing secret" request — marks it handled and removes it from the active list. Use after provisioning the secret (or deciding it should not be provisioned).', {
|
|
75
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
76
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
77
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
78
|
-
request_id: z.string().min(1).describe('Secret-request id'),
|
|
79
|
-
}, async ({ org_slug, project_slug, vault_id, request_id }) => {
|
|
80
|
-
const token = await getToken();
|
|
81
|
-
await apiRequest(`${vaultBase(org_slug, project_slug, vault_id)}/secret-requests/${encodeURIComponent(request_id)}/archive`, { token, method: 'POST' });
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: 'text', text: `Secret request ${request_id} archived.` }],
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type McpServer } from '@ferrlabs/mcp-core';
|
|
2
|
-
interface VaultDetails {
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
description: string | null;
|
|
6
|
-
secret_count: number;
|
|
7
|
-
created_at: string;
|
|
8
|
-
updated_at: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function fetchVaultDetails(orgSlug: string, projectSlug: string, vaultId: string): Promise<VaultDetails>;
|
|
11
|
-
export declare function registerVaultDetailsTool(server: McpServer): void;
|
|
12
|
-
export {};
|
|
13
|
-
//# sourceMappingURL=vault-details.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"vault-details.d.ts","sourceRoot":"","sources":["../../src/tools/vault-details.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAEtF,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,QAgBzD"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { apiRequest, getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
|
-
export async function fetchVaultDetails(orgSlug, projectSlug, vaultId) {
|
|
4
|
-
const token = await getToken();
|
|
5
|
-
return apiRequest(`/orgs/${encodeURIComponent(orgSlug)}/projects/${encodeURIComponent(projectSlug)}/vaults/${encodeURIComponent(vaultId)}`, { token });
|
|
6
|
-
}
|
|
7
|
-
export function registerVaultDetailsTool(server) {
|
|
8
|
-
server.tool('get_vault', 'Get detailed information about a single FerrVault vault — name, description, secret count, timestamps.', {
|
|
9
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
10
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
11
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
12
|
-
}, async ({ org_slug, project_slug, vault_id }) => {
|
|
13
|
-
const vault = await fetchVaultDetails(org_slug, project_slug, vault_id);
|
|
14
|
-
return {
|
|
15
|
-
content: [{ type: 'text', text: toToolText(vault) }],
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
}
|