@ferrlabs/mcp 7.0.1 → 8.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/dist/index.js +2 -0
- package/dist/resources/orgs.d.ts +3 -0
- package/dist/resources/orgs.d.ts.map +1 -0
- package/dist/resources/orgs.js +39 -0
- package/dist/tools/__tests__/tokens.test.d.ts +2 -0
- package/dist/tools/__tests__/tokens.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tokens.test.js +65 -0
- package/dist/tools/issues.js +2 -2
- package/dist/tools/me.d.ts.map +1 -1
- package/dist/tools/me.js +13 -4
- package/dist/tools/org-admin.d.ts +36 -0
- package/dist/tools/org-admin.d.ts.map +1 -1
- package/dist/tools/org-admin.js +30 -17
- package/dist/tools/orgs.d.ts +10 -0
- package/dist/tools/orgs.d.ts.map +1 -1
- package/dist/tools/orgs.js +12 -5
- package/dist/tools/stats.js +3 -3
- package/dist/tools/subscriptions.js +4 -4
- package/dist/tools/tokens.d.ts.map +1 -1
- package/dist/tools/tokens.js +27 -5
- package/dist/tools/vaults.js +2 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerVaultsTools } from './tools/vaults.js';
|
|
|
9
9
|
import { registerIssuesTools } from './tools/issues.js';
|
|
10
10
|
import { registerSubscriptionsTools } from './tools/subscriptions.js';
|
|
11
11
|
import { registerDocsTools } from './tools/docs.js';
|
|
12
|
+
import { registerOrgResources } from './resources/orgs.js';
|
|
12
13
|
runMcp({
|
|
13
14
|
name: 'ferrlabs',
|
|
14
15
|
version: readPackageVersion(import.meta.url),
|
|
@@ -22,6 +23,7 @@ runMcp({
|
|
|
22
23
|
registerIssuesTools(server);
|
|
23
24
|
registerSubscriptionsTools(server);
|
|
24
25
|
registerDocsTools(server);
|
|
26
|
+
registerOrgResources(server);
|
|
25
27
|
},
|
|
26
28
|
}).catch((err) => {
|
|
27
29
|
console.error('ferrlabs-mcp fatal:', err instanceof Error ? err.message : err);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/resources/orgs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAelF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,QAsCrD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ResourceTemplate, toToolText } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { fetchOrgOverview, fetchOrgUsage } from '../tools/org-admin.js';
|
|
3
|
+
import { listOrgSlugs } from '../tools/orgs.js';
|
|
4
|
+
async function listOrgResources(scheme, suffix) {
|
|
5
|
+
const slugs = await listOrgSlugs();
|
|
6
|
+
return {
|
|
7
|
+
resources: slugs.map((slug) => ({
|
|
8
|
+
uri: `${scheme}://org/${slug}/${suffix}`,
|
|
9
|
+
name: `${slug} ${suffix}`,
|
|
10
|
+
mimeType: 'application/json',
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function registerOrgResources(server) {
|
|
15
|
+
server.registerResource('org-overview', new ResourceTemplate('ferrlabs://org/{slug}/overview', {
|
|
16
|
+
list: () => listOrgResources('ferrlabs', 'overview'),
|
|
17
|
+
}), {
|
|
18
|
+
title: 'Organization overview',
|
|
19
|
+
description: 'Member count, active subscriptions per product, pending invites and recent activity volume for one organization.',
|
|
20
|
+
mimeType: 'application/json',
|
|
21
|
+
}, async (uri, { slug }) => {
|
|
22
|
+
const overview = await fetchOrgOverview(String(slug));
|
|
23
|
+
return {
|
|
24
|
+
contents: [{ uri: uri.href, mimeType: 'application/json', text: toToolText(overview) }],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
server.registerResource('org-usage', new ResourceTemplate('ferrlabs://org/{slug}/usage', {
|
|
28
|
+
list: () => listOrgResources('ferrlabs', 'usage'),
|
|
29
|
+
}), {
|
|
30
|
+
title: 'Organization usage',
|
|
31
|
+
description: 'Current-period metered usage across every product for one organization, with the limit for its tier.',
|
|
32
|
+
mimeType: 'application/json',
|
|
33
|
+
}, async (uri, { slug }) => {
|
|
34
|
+
const usage = await fetchOrgUsage(String(slug));
|
|
35
|
+
return {
|
|
36
|
+
contents: [{ uri: uri.href, mimeType: 'application/json', text: toToolText(usage) }],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tokens.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
process.env.FERRLABS_API_TOKEN = 'test-token';
|
|
3
|
+
const handlers = new Map();
|
|
4
|
+
const mockServer = {
|
|
5
|
+
tool: vi.fn((name, _desc, _schema, handler) => {
|
|
6
|
+
handlers.set(name, handler);
|
|
7
|
+
}),
|
|
8
|
+
};
|
|
9
|
+
const mockFetch = vi.fn();
|
|
10
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
11
|
+
function makeResponse(body, status = 200) {
|
|
12
|
+
return {
|
|
13
|
+
ok: status >= 200 && status < 300,
|
|
14
|
+
status,
|
|
15
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
16
|
+
json: () => Promise.resolve(body),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const SECRET = 'fl_live_plaintext_secret_value';
|
|
20
|
+
describe('create_token', () => {
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
handlers.clear();
|
|
24
|
+
delete process.env.FERRLABS_MCP_ALLOW_TOKEN_REVEAL;
|
|
25
|
+
const { registerTokenTools } = await import('../tokens.js');
|
|
26
|
+
registerTokenTools(mockServer);
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
delete process.env.FERRLABS_MCP_ALLOW_TOKEN_REVEAL;
|
|
30
|
+
});
|
|
31
|
+
it('refuses without ever calling the API, so no credential is minted', async () => {
|
|
32
|
+
const result = await handlers.get('create_token')({ name: 'ci', scopes: ['*'] });
|
|
33
|
+
expect(result.isError).toBe(true);
|
|
34
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
it('names the environment variable and the dashboard in the refusal', async () => {
|
|
37
|
+
const result = await handlers.get('create_token')({ name: 'ci', scopes: ['*'] });
|
|
38
|
+
const text = result.content[0].text ?? '';
|
|
39
|
+
expect(text).toMatch(/FERRLABS_MCP_ALLOW_TOKEN_REVEAL=1/);
|
|
40
|
+
expect(text).toMatch(/app\.ferrlabs\.com/);
|
|
41
|
+
});
|
|
42
|
+
it('returns the secret once the operator has opted in', async () => {
|
|
43
|
+
process.env.FERRLABS_MCP_ALLOW_TOKEN_REVEAL = '1';
|
|
44
|
+
mockFetch.mockResolvedValue(makeResponse({
|
|
45
|
+
id: 't1',
|
|
46
|
+
name: 'ci',
|
|
47
|
+
token_prefix: 'fl_live',
|
|
48
|
+
scopes: ['*'],
|
|
49
|
+
expires_at: null,
|
|
50
|
+
last_used_at: null,
|
|
51
|
+
created_at: '2026-09-07T00:00:00Z',
|
|
52
|
+
plaintext: SECRET,
|
|
53
|
+
}));
|
|
54
|
+
const result = await handlers.get('create_token')({ name: 'ci', scopes: ['*'] });
|
|
55
|
+
const text = result.content[0].text ?? '';
|
|
56
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
57
|
+
expect(text).toContain(SECRET);
|
|
58
|
+
expect(text).toMatch(/now in this transcript/);
|
|
59
|
+
});
|
|
60
|
+
it('leaks nothing on the default path', async () => {
|
|
61
|
+
mockFetch.mockResolvedValue(makeResponse({ plaintext: SECRET }));
|
|
62
|
+
const result = await handlers.get('create_token')({ name: 'ci', scopes: ['*'] });
|
|
63
|
+
expect(JSON.stringify(result)).not.toContain(SECRET);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/dist/tools/issues.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { getToken } from '@ferrlabs/mcp-core';
|
|
4
4
|
export function registerIssuesTools(server) {
|
|
5
5
|
server.tool('list_issues', 'List FerrTrack issues for a project (FerrTrack — issue tracker)', {
|
|
@@ -23,7 +23,7 @@ export function registerIssuesTools(server) {
|
|
|
23
23
|
const qs = params.toString();
|
|
24
24
|
const issues = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/projects/${encodeURIComponent(project_slug)}/issues${qs ? `?${qs}` : ''}`, { token });
|
|
25
25
|
return {
|
|
26
|
-
content: [{ type: 'text', text:
|
|
26
|
+
content: [{ type: 'text', text: toToolText(issues) }],
|
|
27
27
|
};
|
|
28
28
|
});
|
|
29
29
|
}
|
package/dist/tools/me.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/tools/me.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BpE,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/tools/me.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BpE,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,QA2EhD"}
|
package/dist/tools/me.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest, getToken } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
export function registerMeTools(server) {
|
|
4
4
|
server.tool('update_me', "Patch the authenticated user's own profile — display name, timezone, locale, avatar URL. Email + password rotate via dedicated flows in the UI, not here.", {
|
|
5
5
|
display_name: z.string().min(1).max(100).optional(),
|
|
@@ -22,12 +22,12 @@ export function registerMeTools(server) {
|
|
|
22
22
|
method: 'PATCH',
|
|
23
23
|
body,
|
|
24
24
|
});
|
|
25
|
-
return { content: [{ type: 'text', text:
|
|
25
|
+
return { content: [{ type: 'text', text: toToolText(me) }] };
|
|
26
26
|
});
|
|
27
27
|
server.tool('list_my_sessions', 'List your active sessions across devices/browsers. The session you used to obtain the current token is flagged `current: true`.', {}, async () => {
|
|
28
28
|
const token = await getToken();
|
|
29
29
|
const sessions = await apiRequest('/me/sessions', { token });
|
|
30
|
-
return { content: [{ type: 'text', text:
|
|
30
|
+
return { content: [{ type: 'text', text: toToolText(sessions) }] };
|
|
31
31
|
});
|
|
32
32
|
server.tool('revoke_my_session', 'Revoke one of your active sessions by id (sign out that device). Refusing to revoke the current session is left up to the API.', {
|
|
33
33
|
session_id: z.string().min(1).describe('Session id (from list_my_sessions)'),
|
|
@@ -42,6 +42,15 @@ export function registerMeTools(server) {
|
|
|
42
42
|
server.tool('export_my_account', "Trigger an export of the authenticated user's data (GDPR Article 15). Returns the export bundle, which can be large; consider redirecting the LLM context to a summary.", {}, async () => {
|
|
43
43
|
const token = await getToken();
|
|
44
44
|
const dump = await apiRequest('/auth/me/export', { token });
|
|
45
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: toToolText(dump, {
|
|
50
|
+
narrowWith: 'Export the account from app.ferrlabs.com to get the bundle whole.',
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
46
55
|
});
|
|
47
56
|
}
|
|
@@ -1,3 +1,39 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
interface Org {
|
|
3
|
+
id: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
plan: string;
|
|
7
|
+
member_count: number;
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OrgOverview {
|
|
12
|
+
org: Org;
|
|
13
|
+
member_count: number;
|
|
14
|
+
active_subscriptions: Array<{
|
|
15
|
+
product: string;
|
|
16
|
+
tier: string;
|
|
17
|
+
status: string;
|
|
18
|
+
}>;
|
|
19
|
+
pending_invites: number;
|
|
20
|
+
recent_activity_count: number;
|
|
21
|
+
}
|
|
22
|
+
export interface OrgUsage {
|
|
23
|
+
org_slug: string;
|
|
24
|
+
period: {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
};
|
|
28
|
+
per_product: Array<{
|
|
29
|
+
product: string;
|
|
30
|
+
metric: string;
|
|
31
|
+
used: number;
|
|
32
|
+
limit: number | null;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export declare function fetchOrgOverview(orgSlug: string): Promise<OrgOverview>;
|
|
36
|
+
export declare function fetchOrgUsage(orgSlug: string): Promise<OrgUsage>;
|
|
2
37
|
export declare function registerOrgAdminTools(server: McpServer): void;
|
|
38
|
+
export {};
|
|
3
39
|
//# sourceMappingURL=org-admin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-admin.d.ts","sourceRoot":"","sources":["../../src/tools/org-admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"org-admin.d.ts","sourceRoot":"","sources":["../../src/tools/org-admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,UAAU,GAAG;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,WAAW,EAAE,KAAK,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB,CAAC,CAAC;CACJ;AAiCD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAG5E;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAGtE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QA6RtD"}
|
package/dist/tools/org-admin.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest, getToken } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
function orgBase(slug) {
|
|
4
4
|
return `/orgs/${encodeURIComponent(slug)}`;
|
|
5
5
|
}
|
|
6
|
+
export async function fetchOrgOverview(orgSlug) {
|
|
7
|
+
const token = await getToken();
|
|
8
|
+
return apiRequest(`${orgBase(orgSlug)}/overview`, { token });
|
|
9
|
+
}
|
|
10
|
+
export async function fetchOrgUsage(orgSlug) {
|
|
11
|
+
const token = await getToken();
|
|
12
|
+
return apiRequest(`${orgBase(orgSlug)}/usage`, { token });
|
|
13
|
+
}
|
|
6
14
|
export function registerOrgAdminTools(server) {
|
|
7
15
|
server.tool('get_org', 'Get a single FerrLabs organization by slug.', {
|
|
8
16
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
9
17
|
}, async ({ org_slug }) => {
|
|
10
18
|
const token = await getToken();
|
|
11
19
|
const org = await apiRequest(orgBase(org_slug), { token });
|
|
12
|
-
return { content: [{ type: 'text', text:
|
|
20
|
+
return { content: [{ type: 'text', text: toToolText(org) }] };
|
|
13
21
|
});
|
|
14
22
|
server.tool('update_org', 'Patch an organization — rename it or change its slug. Slug changes propagate everywhere; double-check before doing it on a live org.', {
|
|
15
23
|
org_slug: z.string().min(1).describe('Current organization slug'),
|
|
@@ -33,21 +41,19 @@ export function registerOrgAdminTools(server) {
|
|
|
33
41
|
method: 'PATCH',
|
|
34
42
|
body,
|
|
35
43
|
});
|
|
36
|
-
return { content: [{ type: 'text', text:
|
|
44
|
+
return { content: [{ type: 'text', text: toToolText(org) }] };
|
|
37
45
|
});
|
|
38
46
|
server.tool('get_org_overview', 'High-level dashboard view of an organization — member count, active subscriptions per product, pending invites, recent activity volume.', {
|
|
39
47
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
40
48
|
}, async ({ org_slug }) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
return { content: [{ type: 'text', text: JSON.stringify(overview, null, 2) }] };
|
|
49
|
+
const overview = await fetchOrgOverview(org_slug);
|
|
50
|
+
return { content: [{ type: 'text', text: toToolText(overview) }] };
|
|
44
51
|
});
|
|
45
52
|
server.tool('get_org_usage', 'Current-period metered usage across all products for an org (FerrGrowth visits/bandwidth, FerrFleet runs, etc.) with limits per tier.', {
|
|
46
53
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
47
54
|
}, async ({ org_slug }) => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
return { content: [{ type: 'text', text: JSON.stringify(usage, null, 2) }] };
|
|
55
|
+
const usage = await fetchOrgUsage(org_slug);
|
|
56
|
+
return { content: [{ type: 'text', text: toToolText(usage) }] };
|
|
51
57
|
});
|
|
52
58
|
server.tool('list_org_audit', 'Recent audit log for an organization — member changes, subscription updates, sensitive admin actions.', {
|
|
53
59
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -62,7 +68,14 @@ export function registerOrgAdminTools(server) {
|
|
|
62
68
|
const token = await getToken();
|
|
63
69
|
const qs = limit !== undefined ? `?limit=${limit}` : '';
|
|
64
70
|
const entries = await apiRequest(`${orgBase(org_slug)}/audit${qs}`, { token });
|
|
65
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: toToolText(entries, { narrowWith: 'Pass a smaller limit.' }),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
66
79
|
});
|
|
67
80
|
// --------------------------------------------------------------------------
|
|
68
81
|
// Members
|
|
@@ -72,7 +85,7 @@ export function registerOrgAdminTools(server) {
|
|
|
72
85
|
}, async ({ org_slug }) => {
|
|
73
86
|
const token = await getToken();
|
|
74
87
|
const members = await apiRequest(`${orgBase(org_slug)}/members`, { token });
|
|
75
|
-
return { content: [{ type: 'text', text:
|
|
88
|
+
return { content: [{ type: 'text', text: toToolText(members) }] };
|
|
76
89
|
});
|
|
77
90
|
server.tool('invite_org_member', 'Invite a user (by email) to join an organization with a specific role. The user receives an invitation email; they appear in list_org_members once they accept.', {
|
|
78
91
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -88,7 +101,7 @@ export function registerOrgAdminTools(server) {
|
|
|
88
101
|
method: 'POST',
|
|
89
102
|
body: { email, role },
|
|
90
103
|
});
|
|
91
|
-
return { content: [{ type: 'text', text:
|
|
104
|
+
return { content: [{ type: 'text', text: toToolText(member) }] };
|
|
92
105
|
});
|
|
93
106
|
server.tool('update_org_member_role', "Change a member's role inside an organization. Promoting to owner requires the caller to be an owner.", {
|
|
94
107
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -97,7 +110,7 @@ export function registerOrgAdminTools(server) {
|
|
|
97
110
|
}, async ({ org_slug, user_id, role }) => {
|
|
98
111
|
const token = await getToken();
|
|
99
112
|
const member = await apiRequest(`${orgBase(org_slug)}/members/${encodeURIComponent(user_id)}`, { token, method: 'PATCH', body: { role } });
|
|
100
|
-
return { content: [{ type: 'text', text:
|
|
113
|
+
return { content: [{ type: 'text', text: toToolText(member) }] };
|
|
101
114
|
});
|
|
102
115
|
server.tool('remove_org_member', 'Remove a user from an organization. Irreversible; the user can be re-invited.', {
|
|
103
116
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -120,7 +133,7 @@ export function registerOrgAdminTools(server) {
|
|
|
120
133
|
}, async ({ org_slug }) => {
|
|
121
134
|
const token = await getToken();
|
|
122
135
|
const teams = await apiRequest(`${orgBase(org_slug)}/teams`, { token });
|
|
123
|
-
return { content: [{ type: 'text', text:
|
|
136
|
+
return { content: [{ type: 'text', text: toToolText(teams) }] };
|
|
124
137
|
});
|
|
125
138
|
server.tool('create_team', 'Create a new team inside an organization. Teams start empty — add members with add_team_member.', {
|
|
126
139
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -133,7 +146,7 @@ export function registerOrgAdminTools(server) {
|
|
|
133
146
|
method: 'POST',
|
|
134
147
|
body: { name, description: description ?? null },
|
|
135
148
|
});
|
|
136
|
-
return { content: [{ type: 'text', text:
|
|
149
|
+
return { content: [{ type: 'text', text: toToolText(team) }] };
|
|
137
150
|
});
|
|
138
151
|
server.tool('update_team', 'Patch a team — rename or change description.', {
|
|
139
152
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -148,7 +161,7 @@ export function registerOrgAdminTools(server) {
|
|
|
148
161
|
body[k] = v;
|
|
149
162
|
}
|
|
150
163
|
const team = await apiRequest(`${orgBase(org_slug)}/teams/${encodeURIComponent(team_id)}`, { token, method: 'PATCH', body });
|
|
151
|
-
return { content: [{ type: 'text', text:
|
|
164
|
+
return { content: [{ type: 'text', text: toToolText(team) }] };
|
|
152
165
|
});
|
|
153
166
|
server.tool('delete_team', 'Delete a team. Members of the team are NOT removed from the org; they just lose team-scoped permissions.', {
|
|
154
167
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -168,7 +181,7 @@ export function registerOrgAdminTools(server) {
|
|
|
168
181
|
}, async ({ org_slug, team_id, user_id }) => {
|
|
169
182
|
const token = await getToken();
|
|
170
183
|
const result = await apiRequest(`${orgBase(org_slug)}/teams/${encodeURIComponent(team_id)}/members`, { token, method: 'POST', body: { user_id } });
|
|
171
|
-
return { content: [{ type: 'text', text:
|
|
184
|
+
return { content: [{ type: 'text', text: toToolText(result) }] };
|
|
172
185
|
});
|
|
173
186
|
server.tool('remove_team_member', 'Remove a user from a team. The user remains an org member.', {
|
|
174
187
|
org_slug: z.string().min(1).describe('Organization slug'),
|
package/dist/tools/orgs.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
interface OrgWithMemberCount {
|
|
3
|
+
id: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
member_count: number;
|
|
7
|
+
created_at: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function fetchOrgs(): Promise<OrgWithMemberCount[]>;
|
|
10
|
+
export declare function listOrgSlugs(): Promise<string[]>;
|
|
2
11
|
export declare function registerOrgsTools(server: McpServer): void;
|
|
12
|
+
export {};
|
|
3
13
|
//# sourceMappingURL=orgs.d.ts.map
|
package/dist/tools/orgs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/tools/orgs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/tools/orgs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,UAAU,kBAAkB;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAWD,wBAAsB,SAAS,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAG/D;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAGtD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,QA8BlD"}
|
package/dist/tools/orgs.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { getToken } from '@ferrlabs/mcp-core';
|
|
4
|
+
export async function fetchOrgs() {
|
|
5
|
+
const token = await getToken();
|
|
6
|
+
return apiRequest('/orgs', { token });
|
|
7
|
+
}
|
|
8
|
+
export async function listOrgSlugs() {
|
|
9
|
+
const orgs = await fetchOrgs();
|
|
10
|
+
return orgs.map((org) => org.slug);
|
|
11
|
+
}
|
|
4
12
|
export function registerOrgsTools(server) {
|
|
5
13
|
server.tool('list_orgs', 'List FerrLabs organizations the authenticated user belongs to', {}, async () => {
|
|
6
|
-
const
|
|
7
|
-
const orgs = await apiRequest('/orgs', { token });
|
|
14
|
+
const orgs = await fetchOrgs();
|
|
8
15
|
return {
|
|
9
|
-
content: [{ type: 'text', text:
|
|
16
|
+
content: [{ type: 'text', text: toToolText(orgs) }],
|
|
10
17
|
};
|
|
11
18
|
});
|
|
12
19
|
server.tool('list_projects', 'List projects inside a FerrLabs organization', {
|
|
@@ -15,7 +22,7 @@ export function registerOrgsTools(server) {
|
|
|
15
22
|
const token = await getToken();
|
|
16
23
|
const projects = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/projects`, { token });
|
|
17
24
|
return {
|
|
18
|
-
content: [{ type: 'text', text:
|
|
25
|
+
content: [{ type: 'text', text: toToolText(projects) }],
|
|
19
26
|
};
|
|
20
27
|
});
|
|
21
28
|
}
|
package/dist/tools/stats.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
1
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
2
2
|
export function registerStatsTools(server) {
|
|
3
3
|
server.tool('get_stats', 'Get public FerrLabs usage statistics (releases cut, repos using FerrFlow, commits analyzed)', {}, async () => {
|
|
4
4
|
const stats = await apiRequest('/stats');
|
|
@@ -6,7 +6,7 @@ export function registerStatsTools(server) {
|
|
|
6
6
|
content: [
|
|
7
7
|
{
|
|
8
8
|
type: 'text',
|
|
9
|
-
text:
|
|
9
|
+
text: toToolText(stats),
|
|
10
10
|
},
|
|
11
11
|
],
|
|
12
12
|
};
|
|
@@ -17,7 +17,7 @@ export function registerStatsTools(server) {
|
|
|
17
17
|
content: [
|
|
18
18
|
{
|
|
19
19
|
type: 'text',
|
|
20
|
-
text:
|
|
20
|
+
text: toToolText(health),
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
23
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { getToken } from '@ferrlabs/mcp-core';
|
|
4
4
|
export function registerSubscriptionsTools(server) {
|
|
5
5
|
server.tool('list_subscriptions', 'List FerrLabs product subscriptions for an organization (FerrVault, FerrTrack, FerrGrowth, FerrFleet)', {
|
|
@@ -8,7 +8,7 @@ export function registerSubscriptionsTools(server) {
|
|
|
8
8
|
const token = await getToken();
|
|
9
9
|
const subs = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/subscriptions`, { token });
|
|
10
10
|
return {
|
|
11
|
-
content: [{ type: 'text', text:
|
|
11
|
+
content: [{ type: 'text', text: toToolText(subs) }],
|
|
12
12
|
};
|
|
13
13
|
});
|
|
14
14
|
const PRODUCTS = ['ferrvault', 'ferrtrack', 'ferrgrowth', 'ferrfleet', 'ferrlens'];
|
|
@@ -20,7 +20,7 @@ export function registerSubscriptionsTools(server) {
|
|
|
20
20
|
}, async ({ org_slug, product, tier }) => {
|
|
21
21
|
const token = await getToken();
|
|
22
22
|
const sub = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/subscriptions`, { token, method: 'POST', body: { product, tier } });
|
|
23
|
-
return { content: [{ type: 'text', text:
|
|
23
|
+
return { content: [{ type: 'text', text: toToolText(sub) }] };
|
|
24
24
|
});
|
|
25
25
|
server.tool('update_subscription', 'Change the tier of an active product subscription (upgrade/downgrade). Stripe proration is applied per the API config.', {
|
|
26
26
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -29,7 +29,7 @@ export function registerSubscriptionsTools(server) {
|
|
|
29
29
|
}, async ({ org_slug, product, tier }) => {
|
|
30
30
|
const token = await getToken();
|
|
31
31
|
const sub = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/subscriptions/${encodeURIComponent(product)}`, { token, method: 'PATCH', body: { tier } });
|
|
32
|
-
return { content: [{ type: 'text', text:
|
|
32
|
+
return { content: [{ type: 'text', text: toToolText(sub) }] };
|
|
33
33
|
});
|
|
34
34
|
server.tool('cancel_subscription', 'Cancel a product subscription on an organization. By default the cancellation takes effect at the end of the current billing period; pass immediate=true to cut it off now (with proration).', {
|
|
35
35
|
org_slug: z.string().min(1).describe('Organization slug'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/tools/tokens.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/tools/tokens.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAmGnD"}
|
package/dist/tools/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { getToken } from '@ferrlabs/mcp-core';
|
|
4
4
|
export function registerTokenTools(server) {
|
|
5
5
|
server.tool('get_me', 'Get the current authenticated FerrLabs user profile', {}, async () => {
|
|
@@ -9,7 +9,7 @@ export function registerTokenTools(server) {
|
|
|
9
9
|
content: [
|
|
10
10
|
{
|
|
11
11
|
type: 'text',
|
|
12
|
-
text:
|
|
12
|
+
text: toToolText(user),
|
|
13
13
|
},
|
|
14
14
|
],
|
|
15
15
|
};
|
|
@@ -21,16 +21,38 @@ export function registerTokenTools(server) {
|
|
|
21
21
|
content: [
|
|
22
22
|
{
|
|
23
23
|
type: 'text',
|
|
24
|
-
text:
|
|
24
|
+
text: toToolText(tokens),
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
27
|
};
|
|
28
28
|
});
|
|
29
|
-
server.tool('create_token', 'Create a new FerrLabs API token
|
|
29
|
+
server.tool('create_token', 'Create a new FerrLabs API token. Disabled unless FERRLABS_MCP_ALLOW_TOKEN_REVEAL=1, because the API shows the secret exactly once and returning it here writes it into the conversation transcript.', {
|
|
30
30
|
name: z.string().min(1).max(100).describe('Token name'),
|
|
31
31
|
scopes: z.array(z.string()).describe('Token scopes (e.g. ["*"] for all)'),
|
|
32
32
|
expires_at: z.string().optional().describe('Expiration date (ISO 8601)'),
|
|
33
33
|
}, async ({ name, scopes, expires_at }) => {
|
|
34
|
+
// Refuse before minting, not after. The API returns the plaintext once
|
|
35
|
+
// and never again, so creating the token and then withholding the
|
|
36
|
+
// secret would leave a live credential nobody can use.
|
|
37
|
+
if (process.env.FERRLABS_MCP_ALLOW_TOKEN_REVEAL !== '1') {
|
|
38
|
+
return {
|
|
39
|
+
isError: true,
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: [
|
|
44
|
+
'Refusing to create an API token.',
|
|
45
|
+
'',
|
|
46
|
+
'The secret is returned exactly once, at creation, so this tool would have to put it in its response — and tool results are written to the conversation transcript, which the client persists to disk and may ship in logs or telemetry. A long-lived credential would end up in places you did not choose.',
|
|
47
|
+
'',
|
|
48
|
+
'Create the token from app.ferrlabs.com → Settings → API Tokens instead.',
|
|
49
|
+
'',
|
|
50
|
+
'If you accept the exposure, restart the MCP server with FERRLABS_MCP_ALLOW_TOKEN_REVEAL=1. It is an environment variable rather than an argument on purpose: the decision belongs to whoever runs the server.',
|
|
51
|
+
].join('\n'),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
34
56
|
const token = await getToken();
|
|
35
57
|
const result = await apiRequest('/auth/tokens', {
|
|
36
58
|
method: 'POST',
|
|
@@ -42,7 +64,7 @@ export function registerTokenTools(server) {
|
|
|
42
64
|
content: [
|
|
43
65
|
{
|
|
44
66
|
type: 'text',
|
|
45
|
-
text: `Token created: ${plaintext}\n\nThis is the only time the
|
|
67
|
+
text: `Token created: ${plaintext}\n\nThis is the only time the secret is shown, and it is now in this transcript. Move it to your secret store, then treat the transcript as sensitive or revoke the token with revoke_token.\n\n${toToolText(meta)}`,
|
|
46
68
|
},
|
|
47
69
|
],
|
|
48
70
|
};
|
package/dist/tools/vaults.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { apiRequest, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { getToken } from '@ferrlabs/mcp-core';
|
|
4
4
|
export function registerVaultsTools(server) {
|
|
5
5
|
server.tool('list_vaults', 'List FerrVault vaults inside a project (FerrVault — secrets management)', {
|
|
@@ -9,7 +9,7 @@ export function registerVaultsTools(server) {
|
|
|
9
9
|
const token = await getToken();
|
|
10
10
|
const vaults = await apiRequest(`/orgs/${encodeURIComponent(org_slug)}/projects/${encodeURIComponent(project_slug)}/vaults`, { token });
|
|
11
11
|
return {
|
|
12
|
-
content: [{ type: 'text', text:
|
|
12
|
+
content: [{ type: 'text', text: toToolText(vaults) }],
|
|
13
13
|
};
|
|
14
14
|
});
|
|
15
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferrlabs/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.2.1",
|
|
4
4
|
"description": "Model Context Protocol server for the FerrLabs platform (orgs, projects, subscriptions, docs, tokens, public stats)",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"ai"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@ferrlabs/mcp-core": "^8.2.1",
|
|
24
25
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
25
|
-
"zod": "^4.3.6"
|
|
26
|
-
"@ferrlabs/mcp-core": "^7.0.1"
|
|
26
|
+
"zod": "^4.3.6"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|