adoptai-mcp 1.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/README.md +70 -0
- package/bin/adoptai-mcp.js +2 -0
- package/dist/apps/canva.js +1 -0
- package/dist/apps/figma.js +1 -0
- package/dist/apps/github.js +2 -0
- package/dist/apps/notion.js +1 -0
- package/dist/apps/registry.js +20 -0
- package/dist/apps/salesforce.js +1 -0
- package/dist/cli/add.js +532 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/list.js +19 -0
- package/dist/cli/remove.js +37 -0
- package/dist/cli/serve.js +27 -0
- package/dist/cli/status.js +24 -0
- package/dist/config/clients.js +118 -0
- package/dist/config/credentials.js +34 -0
- package/dist/core/auth-manager.js +237 -0
- package/dist/core/config-writer.js +161 -0
- package/dist/core/doctor.js +199 -0
- package/dist/core/package.json +3 -0
- package/dist/core/server-base.js +81 -0
- package/dist/integrations/canva/.env +3 -0
- package/dist/integrations/canva/auth.js +287 -0
- package/dist/integrations/canva/env.js +9 -0
- package/dist/integrations/canva/index.js +12 -0
- package/dist/integrations/canva/package.json +31 -0
- package/dist/integrations/canva/publish-to-adoptai.js +365 -0
- package/dist/integrations/canva/setup.js +90 -0
- package/dist/integrations/canva/tools.js +1315 -0
- package/dist/integrations/canva/tools.original.js +1315 -0
- package/dist/integrations/figma/auth.js +48 -0
- package/dist/integrations/figma/index.js +11 -0
- package/dist/integrations/figma/package.json +27 -0
- package/dist/integrations/figma/publish-to-adoptai.js +384 -0
- package/dist/integrations/figma/setup.js +90 -0
- package/dist/integrations/figma/tools.js +1137 -0
- package/dist/integrations/github/auth.js +53 -0
- package/dist/integrations/github/index.js +11 -0
- package/dist/integrations/github/package.json +28 -0
- package/dist/integrations/github/publish-to-adoptai.js +240 -0
- package/dist/integrations/github/setup.js +103 -0
- package/dist/integrations/github/tools.js +78 -0
- package/dist/integrations/github-actions/auth.js +53 -0
- package/dist/integrations/github-actions/index.js +11 -0
- package/dist/integrations/github-actions/package.json +27 -0
- package/dist/integrations/github-actions/setup.js +103 -0
- package/dist/integrations/github-actions/tools.js +5642 -0
- package/dist/integrations/github-activity/auth.js +53 -0
- package/dist/integrations/github-activity/index.js +11 -0
- package/dist/integrations/github-activity/package.json +27 -0
- package/dist/integrations/github-activity/setup.js +103 -0
- package/dist/integrations/github-activity/tools.js +925 -0
- package/dist/integrations/github-apps/auth.js +53 -0
- package/dist/integrations/github-apps/index.js +11 -0
- package/dist/integrations/github-apps/package.json +27 -0
- package/dist/integrations/github-apps/setup.js +103 -0
- package/dist/integrations/github-apps/tools.js +791 -0
- package/dist/integrations/github-billing/auth.js +53 -0
- package/dist/integrations/github-billing/index.js +11 -0
- package/dist/integrations/github-billing/package.json +27 -0
- package/dist/integrations/github-billing/setup.js +103 -0
- package/dist/integrations/github-billing/tools.js +438 -0
- package/dist/integrations/github-checks/auth.js +53 -0
- package/dist/integrations/github-checks/index.js +11 -0
- package/dist/integrations/github-checks/package.json +27 -0
- package/dist/integrations/github-checks/setup.js +103 -0
- package/dist/integrations/github-checks/tools.js +607 -0
- package/dist/integrations/github-code-scanning/auth.js +53 -0
- package/dist/integrations/github-code-scanning/index.js +11 -0
- package/dist/integrations/github-code-scanning/package.json +27 -0
- package/dist/integrations/github-code-scanning/setup.js +103 -0
- package/dist/integrations/github-code-scanning/tools.js +987 -0
- package/dist/integrations/github-dependabot/auth.js +53 -0
- package/dist/integrations/github-dependabot/index.js +11 -0
- package/dist/integrations/github-dependabot/package.json +27 -0
- package/dist/integrations/github-dependabot/setup.js +103 -0
- package/dist/integrations/github-dependabot/tools.js +915 -0
- package/dist/integrations/github-gists/auth.js +53 -0
- package/dist/integrations/github-gists/index.js +11 -0
- package/dist/integrations/github-gists/package.json +27 -0
- package/dist/integrations/github-gists/setup.js +103 -0
- package/dist/integrations/github-gists/tools.js +545 -0
- package/dist/integrations/github-git/auth.js +53 -0
- package/dist/integrations/github-git/index.js +11 -0
- package/dist/integrations/github-git/package.json +27 -0
- package/dist/integrations/github-git/setup.js +103 -0
- package/dist/integrations/github-git/tools.js +513 -0
- package/dist/integrations/github-issues/auth.js +53 -0
- package/dist/integrations/github-issues/index.js +11 -0
- package/dist/integrations/github-issues/package.json +27 -0
- package/dist/integrations/github-issues/setup.js +103 -0
- package/dist/integrations/github-issues/tools.js +2232 -0
- package/dist/integrations/github-orgs/auth.js +53 -0
- package/dist/integrations/github-orgs/index.js +11 -0
- package/dist/integrations/github-orgs/package.json +27 -0
- package/dist/integrations/github-orgs/setup.js +103 -0
- package/dist/integrations/github-orgs/tools.js +3512 -0
- package/dist/integrations/github-packages/auth.js +53 -0
- package/dist/integrations/github-packages/index.js +11 -0
- package/dist/integrations/github-packages/package.json +27 -0
- package/dist/integrations/github-packages/setup.js +103 -0
- package/dist/integrations/github-packages/tools.js +1088 -0
- package/dist/integrations/github-pulls/auth.js +53 -0
- package/dist/integrations/github-pulls/index.js +11 -0
- package/dist/integrations/github-pulls/package.json +27 -0
- package/dist/integrations/github-pulls/setup.js +103 -0
- package/dist/integrations/github-pulls/tools.js +1252 -0
- package/dist/integrations/github-reactions/auth.js +53 -0
- package/dist/integrations/github-reactions/index.js +11 -0
- package/dist/integrations/github-reactions/package.json +27 -0
- package/dist/integrations/github-reactions/setup.js +103 -0
- package/dist/integrations/github-reactions/tools.js +706 -0
- package/dist/integrations/github-repos/auth.js +53 -0
- package/dist/integrations/github-repos/index.js +11 -0
- package/dist/integrations/github-repos/package.json +27 -0
- package/dist/integrations/github-repos/setup.js +103 -0
- package/dist/integrations/github-repos/tools.js +7286 -0
- package/dist/integrations/github-search/auth.js +53 -0
- package/dist/integrations/github-search/index.js +11 -0
- package/dist/integrations/github-search/package.json +27 -0
- package/dist/integrations/github-search/setup.js +103 -0
- package/dist/integrations/github-search/tools.js +370 -0
- package/dist/integrations/github-teams/auth.js +53 -0
- package/dist/integrations/github-teams/index.js +11 -0
- package/dist/integrations/github-teams/package.json +27 -0
- package/dist/integrations/github-teams/setup.js +103 -0
- package/dist/integrations/github-teams/tools.js +633 -0
- package/dist/integrations/github-users/auth.js +53 -0
- package/dist/integrations/github-users/index.js +11 -0
- package/dist/integrations/github-users/package.json +27 -0
- package/dist/integrations/github-users/setup.js +103 -0
- package/dist/integrations/github-users/tools.js +1118 -0
- package/dist/integrations/notion/api.js +108 -0
- package/dist/integrations/notion/auth.js +59 -0
- package/dist/integrations/notion/endpoints.json +630 -0
- package/dist/integrations/notion/index.js +11 -0
- package/dist/integrations/notion/package.json +33 -0
- package/dist/integrations/notion/publish-to-adoptai.js +271 -0
- package/dist/integrations/notion/scripts/generate-endpoints.mjs +306 -0
- package/dist/integrations/notion/setup.js +89 -0
- package/dist/integrations/notion/tools.js +586 -0
- package/dist/integrations/notion/tools.original.js +568 -0
- package/dist/integrations/salesforce/.env +8 -0
- package/dist/integrations/salesforce/.env.example +15 -0
- package/dist/integrations/salesforce/auth.js +311 -0
- package/dist/integrations/salesforce/endpoints.json +1359 -0
- package/dist/integrations/salesforce/env.js +9 -0
- package/dist/integrations/salesforce/index.js +12 -0
- package/dist/integrations/salesforce/package.json +42 -0
- package/dist/integrations/salesforce/publish-smart-specs.js +890 -0
- package/dist/integrations/salesforce/publish-to-adoptai.js +386 -0
- package/dist/integrations/salesforce/scripts/extract-postman.mjs +222 -0
- package/dist/integrations/salesforce/setup.js +112 -0
- package/dist/integrations/salesforce/tools.js +4544 -0
- package/dist/integrations/salesforce/tools.original.js +4487 -0
- package/dist/server/mcp-server.js +50 -0
- package/dist/server/tool-loader.js +47 -0
- package/dist/specs/figma-api.json +13621 -0
- package/dist/specs/split/salesforce-auth.json +3931 -0
- package/dist/specs/split/salesforce-bulk-v1.json +1489 -0
- package/dist/specs/split/salesforce-bulk-v2.json +1951 -0
- package/dist/specs/split/salesforce-composite.json +1246 -0
- package/dist/specs/split/salesforce-connect.json +11639 -0
- package/dist/specs/split/salesforce-einstein-prediction-service.json +576 -0
- package/dist/specs/split/salesforce-event-platform.json +2682 -0
- package/dist/specs/split/salesforce-graphql.json +1754 -0
- package/dist/specs/split/salesforce-industries.json +4115 -0
- package/dist/specs/split/salesforce-metadata.json +555 -0
- package/dist/specs/split/salesforce-rest.json +4798 -0
- package/dist/specs/split/salesforce-soap.json +210 -0
- package/dist/specs/split/salesforce-subscription-management.json +1299 -0
- package/dist/specs/split/salesforce-tooling.json +2026 -0
- package/dist/specs/split/salesforce-ui.json +7426 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { buildAuthHeaders } from './auth.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const spec = JSON.parse(readFileSync(join(__dirname, '../../specs/figma-api.json'), 'utf8'));
|
|
9
|
+
|
|
10
|
+
const BASE_V1 = 'https://api.figma.com/v1';
|
|
11
|
+
const BASE_V2 = 'https://api.figma.com/v2';
|
|
12
|
+
|
|
13
|
+
const EXCLUDED_OPS = new Set([
|
|
14
|
+
'GET /v1/files/{file_key}',
|
|
15
|
+
'GET /v1/files/{file_key}/nodes',
|
|
16
|
+
'GET /v1/images/{file_key}',
|
|
17
|
+
'GET /v1/files/{file_key}/versions',
|
|
18
|
+
'GET /v1/files/{file_key}/components',
|
|
19
|
+
'GET /v1/files/{file_key}/comments',
|
|
20
|
+
'POST /v1/files/{file_key}/comments',
|
|
21
|
+
'DELETE /v1/files/{file_key}/comments/{comment_id}',
|
|
22
|
+
'GET /v1/teams/{team_id}/components',
|
|
23
|
+
'GET /v1/components/{key}',
|
|
24
|
+
'GET /v1/teams/{team_id}/component_sets',
|
|
25
|
+
'GET /v1/teams/{team_id}/styles',
|
|
26
|
+
'GET /v1/files/{file_key}/variables/local',
|
|
27
|
+
'GET /v1/files/{file_key}/variables/published',
|
|
28
|
+
'POST /v1/files/{file_key}/variables',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function opKey(method, path) {
|
|
32
|
+
return `${method} ${path}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function text(payload) {
|
|
36
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleError(err) {
|
|
40
|
+
const status = err.response?.status;
|
|
41
|
+
const data = err.response?.data;
|
|
42
|
+
const msg = data?.message || data?.err || data?.error || err.message;
|
|
43
|
+
if (status === 401) throw new Error('Token invalid.\nRun: npx adoptai-figma-mcp --client cursor');
|
|
44
|
+
if (status === 403) throw new Error('Insufficient permissions. Check your token scopes.');
|
|
45
|
+
if (status === 404) throw new Error('Resource not found. Check your parameters.');
|
|
46
|
+
if (status === 429) throw new Error('Rate limit exceeded. Please wait and try again.');
|
|
47
|
+
throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg) || 'API request failed');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveUrlParts(specPath) {
|
|
51
|
+
if (specPath.startsWith('/v2/')) return { base: BASE_V2, rel: specPath.slice(3) };
|
|
52
|
+
if (specPath.startsWith('/v1/')) return { base: BASE_V1, rel: specPath.slice(3) };
|
|
53
|
+
return { base: BASE_V1, rel: specPath.startsWith('/') ? specPath : `/${specPath}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scrub(params) {
|
|
57
|
+
const o = {};
|
|
58
|
+
for (const [k, v] of Object.entries(params)) {
|
|
59
|
+
if (v !== undefined && k !== '_rawBody') o[k] = v;
|
|
60
|
+
}
|
|
61
|
+
return o;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function apiRequest(method, specPath, params = {}) {
|
|
65
|
+
const { base, rel } = resolveUrlParts(specPath);
|
|
66
|
+
let url = base + rel;
|
|
67
|
+
const headers = { 'Content-Type': 'application/json', ...buildAuthHeaders() };
|
|
68
|
+
const queryParams = {};
|
|
69
|
+
const bodyParams = {};
|
|
70
|
+
const p = typeof params === 'object' && params ? { ...params } : {};
|
|
71
|
+
|
|
72
|
+
for (const [key, value] of Object.entries(p)) {
|
|
73
|
+
if (key === '_rawBody') continue;
|
|
74
|
+
if (value === undefined) continue;
|
|
75
|
+
const placeholder = `{${key}}`;
|
|
76
|
+
if (url.includes(placeholder)) {
|
|
77
|
+
url = url.split(placeholder).join(encodeURIComponent(String(value)));
|
|
78
|
+
} else if (['GET', 'DELETE'].includes(method)) {
|
|
79
|
+
queryParams[key] = value;
|
|
80
|
+
} else {
|
|
81
|
+
bodyParams[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let data;
|
|
86
|
+
if (p._rawBody != null && typeof p._rawBody === 'object') {
|
|
87
|
+
data = p._rawBody;
|
|
88
|
+
} else if (Object.keys(bodyParams).length) {
|
|
89
|
+
data = bodyParams;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await axios({
|
|
94
|
+
method,
|
|
95
|
+
url,
|
|
96
|
+
headers,
|
|
97
|
+
params: Object.keys(queryParams).length ? queryParams : undefined,
|
|
98
|
+
data,
|
|
99
|
+
timeout: 120000,
|
|
100
|
+
});
|
|
101
|
+
return res.data;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
handleError(err);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveRef(root, ref) {
|
|
108
|
+
if (!ref || !ref.startsWith('#/')) return null;
|
|
109
|
+
const parts = ref.replace('#/', '').split('/');
|
|
110
|
+
let cur = root;
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
cur = cur?.[part];
|
|
113
|
+
}
|
|
114
|
+
return cur;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function operationIdToName(operationId) {
|
|
118
|
+
if (!operationId) return null;
|
|
119
|
+
return operationId
|
|
120
|
+
.replace(/([A-Z])/g, '_$1')
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/^_/, '')
|
|
123
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
124
|
+
.replace(/_+/g, '_')
|
|
125
|
+
.substring(0, 60);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeSchemaFromBody(specRoot, schema, properties, required) {
|
|
129
|
+
if (!schema) return;
|
|
130
|
+
if (schema.$ref) {
|
|
131
|
+
mergeSchemaFromBody(specRoot, resolveRef(specRoot, schema.$ref), properties, required);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (schema.allOf) {
|
|
135
|
+
for (const sub of schema.allOf) mergeSchemaFromBody(specRoot, sub, properties, required);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (schema.properties) {
|
|
139
|
+
for (const [k, val] of Object.entries(schema.properties)) {
|
|
140
|
+
if (properties[k]) continue;
|
|
141
|
+
const t = val?.type || 'string';
|
|
142
|
+
properties[k] = {
|
|
143
|
+
type: Array.isArray(t) ? t[0] : t,
|
|
144
|
+
description: val?.description || `The ${k} field`,
|
|
145
|
+
};
|
|
146
|
+
if (val?.enum) properties[k].enum = val.enum;
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(schema.required)) {
|
|
149
|
+
for (const r of schema.required) required.add(r);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildInputSchemaForOperation(specRoot, method, op) {
|
|
155
|
+
const properties = {};
|
|
156
|
+
const required = new Set();
|
|
157
|
+
|
|
158
|
+
for (const rawParam of op.parameters || []) {
|
|
159
|
+
let param = rawParam;
|
|
160
|
+
if (param.$ref) param = resolveRef(specRoot, param.$ref);
|
|
161
|
+
if (!param?.name) continue;
|
|
162
|
+
const t = param.schema?.type || param.type || 'string';
|
|
163
|
+
properties[param.name] = {
|
|
164
|
+
type: Array.isArray(t) ? t[0] : t,
|
|
165
|
+
description: param.description || `The ${param.name} parameter`,
|
|
166
|
+
};
|
|
167
|
+
if (param.schema?.enum) properties[param.name].enum = param.schema.enum;
|
|
168
|
+
if (param.required) required.add(param.name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bodySchema =
|
|
172
|
+
op.requestBody?.content?.['application/json']?.schema ||
|
|
173
|
+
op.requestBody?.content?.['application/*+json']?.schema;
|
|
174
|
+
|
|
175
|
+
mergeSchemaFromBody(specRoot, bodySchema, properties, required);
|
|
176
|
+
|
|
177
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
178
|
+
properties._rawBody = {
|
|
179
|
+
type: 'object',
|
|
180
|
+
description: 'Optional full JSON body; when set, replaces other body fields.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { type: 'object', properties, required: [...required] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function methodPriority(m) {
|
|
188
|
+
const order = { GET: 0, POST: 1, PUT: 2, PATCH: 3, DELETE: 4 };
|
|
189
|
+
return order[m] ?? 5;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildGeneratedTools() {
|
|
193
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
194
|
+
const items = [];
|
|
195
|
+
|
|
196
|
+
for (const [specPath, pathItem] of Object.entries(spec.paths || {})) {
|
|
197
|
+
if (specPath.includes('/oauth/') && (specPath.endsWith('/token') || specPath.includes('/refresh'))) continue;
|
|
198
|
+
|
|
199
|
+
for (const method of HTTP_METHODS) {
|
|
200
|
+
const op = pathItem[method];
|
|
201
|
+
if (!op || op.deprecated) continue;
|
|
202
|
+
const M = method.toUpperCase();
|
|
203
|
+
if (EXCLUDED_OPS.has(opKey(M, specPath))) continue;
|
|
204
|
+
|
|
205
|
+
const name =
|
|
206
|
+
operationIdToName(op.operationId) ||
|
|
207
|
+
`figma_${M.toLowerCase()}_${specPath.replace(/[^a-z0-9]+/gi, '_')}`;
|
|
208
|
+
const description = (op.summary || op.description || `${M} ${specPath}`).slice(0, 800);
|
|
209
|
+
|
|
210
|
+
items.push({
|
|
211
|
+
sort: [methodPriority(M), specPath],
|
|
212
|
+
tool: {
|
|
213
|
+
name,
|
|
214
|
+
description,
|
|
215
|
+
inputSchema: buildInputSchemaForOperation(spec, M, op),
|
|
216
|
+
handler: async (params) => text(await apiRequest(M, specPath, params)),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
items.sort((a, b) => (a.sort[0] !== b.sort[0] ? a.sort[0] - b.sort[0] : a.sort[1].localeCompare(b.sort[1])));
|
|
223
|
+
return items.map((i) => i.tool);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function walkNodes(node, fn, path = []) {
|
|
227
|
+
if (!node) return;
|
|
228
|
+
fn(node, path);
|
|
229
|
+
const ch = node.children;
|
|
230
|
+
if (Array.isArray(ch)) {
|
|
231
|
+
for (const c of ch) walkNodes(c, fn, [...path, node.name || node.id || '']);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function nameMatch(query, name) {
|
|
236
|
+
if (!name) return false;
|
|
237
|
+
const pattern = query
|
|
238
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
239
|
+
.replace(/\*/g, '.*')
|
|
240
|
+
.replace(/\?/g, '.');
|
|
241
|
+
try {
|
|
242
|
+
return new RegExp(`^${pattern}$`, 'i').test(name);
|
|
243
|
+
} catch {
|
|
244
|
+
return name.toLowerCase().includes(query.toLowerCase());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseFigmaUrl(figmaUrl) {
|
|
249
|
+
const out = { file_key: null, node_id: null, team_id: null, project_id: null };
|
|
250
|
+
try {
|
|
251
|
+
const u = new URL(figmaUrl.trim());
|
|
252
|
+
const path = u.pathname;
|
|
253
|
+
const fileMatch = path.match(/\/(?:file|design|proto)\/([a-zA-Z0-9]+)/);
|
|
254
|
+
if (fileMatch) out.file_key = fileMatch[1];
|
|
255
|
+
const teamMatch = path.match(/\/team\/([a-zA-Z0-9]+)/);
|
|
256
|
+
if (teamMatch) out.team_id = teamMatch[1];
|
|
257
|
+
const projMatch = path.match(/\/project\/([a-zA-Z0-9]+)/);
|
|
258
|
+
if (projMatch) out.project_id = projMatch[1];
|
|
259
|
+
const nid = u.searchParams.get('node-id');
|
|
260
|
+
if (nid) out.node_id = decodeURIComponent(nid).replace(/-/g, ':');
|
|
261
|
+
} catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function extractTokensPayload(file_key, token_format = 'json') {
|
|
268
|
+
const [localVars, publishedVars, styles, components] = await Promise.all([
|
|
269
|
+
apiRequest('GET', '/v1/files/{file_key}/variables/local', { file_key }),
|
|
270
|
+
apiRequest('GET', '/v1/files/{file_key}/variables/published', { file_key }),
|
|
271
|
+
apiRequest('GET', '/v1/files/{file_key}/styles', { file_key }),
|
|
272
|
+
apiRequest('GET', '/v1/files/{file_key}/components', { file_key }),
|
|
273
|
+
]);
|
|
274
|
+
return {
|
|
275
|
+
meta: { file_key, token_format },
|
|
276
|
+
localVariables: localVars,
|
|
277
|
+
publishedVariables: publishedVars,
|
|
278
|
+
styles,
|
|
279
|
+
components,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildTailwindThemeFromTokens(payload) {
|
|
284
|
+
const theme = { extend: { colors: {}, fontSize: {}, boxShadow: {} } };
|
|
285
|
+
const metaStyles = payload.styles?.meta?.styles || [];
|
|
286
|
+
for (const s of metaStyles) {
|
|
287
|
+
if (s.styleType === 'FILL' && s.name) {
|
|
288
|
+
const key = s.name.replace(/\s+/g, '-').toLowerCase();
|
|
289
|
+
theme.extend.colors[`figma-${key}`] = `var(--figma-style-${(s.key || key).toString().slice(0, 12)})`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return theme;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const CUSTOM_TOOLS_FRONT = [
|
|
296
|
+
{
|
|
297
|
+
name: 'get_file',
|
|
298
|
+
description:
|
|
299
|
+
"Retrieves a Figma file's full document tree including all nodes, styles and components. Use when you need the complete file structure. For just the node tree outline use get_file_nodes. Depth controls how deep the node tree goes — use depth=1 for top-level frames only.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
304
|
+
depth: { type: 'number', description: 'Tree depth (default 2)', default: 2 },
|
|
305
|
+
geometry: { type: 'string', description: 'Set to "paths" for vector data' },
|
|
306
|
+
version: { type: 'string', description: 'Version id' },
|
|
307
|
+
},
|
|
308
|
+
required: ['file_key'],
|
|
309
|
+
},
|
|
310
|
+
handler: async (p) => {
|
|
311
|
+
const { file_key, depth = 2, geometry, version } = p;
|
|
312
|
+
return text(await apiRequest('GET', '/v1/files/{file_key}', scrub({ file_key, depth, geometry, version })));
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'get_file_pages',
|
|
317
|
+
description:
|
|
318
|
+
'Retrieves only the top-level pages of a Figma file without loading all node content. Much faster than get_file for large files. Use when you need to list pages before drilling into a specific one.',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
322
|
+
required: ['file_key'],
|
|
323
|
+
},
|
|
324
|
+
handler: async (p) =>
|
|
325
|
+
text(await apiRequest('GET', '/v1/files/{file_key}', { file_key: p.file_key, depth: 1 })),
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'get_file_components_summary',
|
|
329
|
+
description:
|
|
330
|
+
'Returns all published components in a Figma file with their names, node IDs and descriptions. Use to discover available components before fetching specific node details.',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
334
|
+
required: ['file_key'],
|
|
335
|
+
},
|
|
336
|
+
handler: async (p) =>
|
|
337
|
+
text(await apiRequest('GET', '/v1/files/{file_key}/components', { file_key: p.file_key })),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'get_file_versions',
|
|
341
|
+
description:
|
|
342
|
+
'Returns the version history of a Figma file with timestamps, author and description for each version. Use to review file history or revert to a specific version.',
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
346
|
+
required: ['file_key'],
|
|
347
|
+
},
|
|
348
|
+
handler: async (p) =>
|
|
349
|
+
text(await apiRequest('GET', '/v1/files/{file_key}/versions', { file_key: p.file_key })),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'get_file_nodes',
|
|
353
|
+
description:
|
|
354
|
+
'Retrieves specific nodes from a Figma file by their IDs. More efficient than get_file when you only need specific layers. Use after get_file_pages to drill into specific frames.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
359
|
+
ids: { type: 'string', description: 'Comma-separated node IDs' },
|
|
360
|
+
depth: { type: 'number', description: 'Optional depth' },
|
|
361
|
+
geometry: { type: 'string', description: 'Optional geometry e.g. paths' },
|
|
362
|
+
},
|
|
363
|
+
required: ['file_key', 'ids'],
|
|
364
|
+
},
|
|
365
|
+
handler: async (p) =>
|
|
366
|
+
text(
|
|
367
|
+
await apiRequest(
|
|
368
|
+
'GET',
|
|
369
|
+
'/v1/files/{file_key}/nodes',
|
|
370
|
+
scrub({ file_key: p.file_key, ids: p.ids, depth: p.depth, geometry: p.geometry })
|
|
371
|
+
)
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'get_node_css',
|
|
376
|
+
description:
|
|
377
|
+
'Retrieves a specific node with full geometry and path data needed to extract CSS properties. Use when you need to generate CSS from a Figma design element.',
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: 'object',
|
|
380
|
+
properties: {
|
|
381
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
382
|
+
node_id: { type: 'string', description: 'Node id e.g. 1:2' },
|
|
383
|
+
},
|
|
384
|
+
required: ['file_key', 'node_id'],
|
|
385
|
+
},
|
|
386
|
+
handler: async (p) =>
|
|
387
|
+
text(
|
|
388
|
+
await apiRequest('GET', '/v1/files/{file_key}/nodes', {
|
|
389
|
+
file_key: p.file_key,
|
|
390
|
+
ids: p.node_id,
|
|
391
|
+
geometry: 'paths',
|
|
392
|
+
})
|
|
393
|
+
),
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'get_frame_by_name',
|
|
397
|
+
description:
|
|
398
|
+
'Finds a specific frame or layer in a Figma file by its name and returns its node data. Useful when you know the frame name but not its node ID.',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
403
|
+
frame_name: { type: 'string', description: 'Exact or wildcard name (*, ?)' },
|
|
404
|
+
page_name: { type: 'string', description: 'Optional page name filter' },
|
|
405
|
+
},
|
|
406
|
+
required: ['file_key', 'frame_name'],
|
|
407
|
+
},
|
|
408
|
+
handler: async (p) => {
|
|
409
|
+
const doc = await apiRequest('GET', '/v1/files/{file_key}', { file_key: p.file_key, depth: 20 });
|
|
410
|
+
const matches = [];
|
|
411
|
+
walkNodes(doc.document, (node, pathNames) => {
|
|
412
|
+
const onPage =
|
|
413
|
+
!p.page_name || pathNames[0] === p.page_name || pathNames.join('/').includes(p.page_name);
|
|
414
|
+
if (!onPage) return;
|
|
415
|
+
if (node.name && nameMatch(p.frame_name, node.name)) {
|
|
416
|
+
matches.push({
|
|
417
|
+
id: node.id,
|
|
418
|
+
name: node.name,
|
|
419
|
+
type: node.type,
|
|
420
|
+
path: pathNames.filter(Boolean).join(' / '),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (matches.length === 0) {
|
|
425
|
+
return text({ matches: [], note: 'No nodes matched; try search_nodes_by_name or adjust depth.' });
|
|
426
|
+
}
|
|
427
|
+
const ids = matches.map((m) => m.id).join(',');
|
|
428
|
+
const detail = await apiRequest('GET', '/v1/files/{file_key}/nodes', { file_key: p.file_key, ids });
|
|
429
|
+
return text({ matches, nodes: detail });
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'export_node_as_png',
|
|
434
|
+
description:
|
|
435
|
+
'Exports a specific Figma node as a PNG image at a specified scale. Returns the download URL. Use for exporting icons, illustrations or screen designs as PNG.',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
440
|
+
node_id: { type: 'string', description: 'Node id' },
|
|
441
|
+
scale: { type: 'number', description: 'Scale 0.01–4 (default 2)', default: 2 },
|
|
442
|
+
use_absolute_bounds: { type: 'boolean', description: 'Use full node bounds' },
|
|
443
|
+
},
|
|
444
|
+
required: ['file_key', 'node_id'],
|
|
445
|
+
},
|
|
446
|
+
handler: async (p) =>
|
|
447
|
+
text(
|
|
448
|
+
await apiRequest(
|
|
449
|
+
'GET',
|
|
450
|
+
'/v1/images/{file_key}',
|
|
451
|
+
scrub({
|
|
452
|
+
file_key: p.file_key,
|
|
453
|
+
ids: p.node_id,
|
|
454
|
+
format: 'png',
|
|
455
|
+
scale: p.scale ?? 2,
|
|
456
|
+
use_absolute_bounds: p.use_absolute_bounds,
|
|
457
|
+
})
|
|
458
|
+
)
|
|
459
|
+
),
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: 'export_node_as_svg',
|
|
463
|
+
description:
|
|
464
|
+
'Exports a Figma node as an SVG file. SVG is best for icons and vector illustrations that need to scale. Returns the download URL.',
|
|
465
|
+
inputSchema: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
properties: {
|
|
468
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
469
|
+
node_id: { type: 'string', description: 'Node id' },
|
|
470
|
+
svg_outline_text: { type: 'boolean', description: 'Outline text as paths vs text elements' },
|
|
471
|
+
},
|
|
472
|
+
required: ['file_key', 'node_id'],
|
|
473
|
+
},
|
|
474
|
+
handler: async (p) =>
|
|
475
|
+
text(
|
|
476
|
+
await apiRequest(
|
|
477
|
+
'GET',
|
|
478
|
+
'/v1/images/{file_key}',
|
|
479
|
+
scrub({
|
|
480
|
+
file_key: p.file_key,
|
|
481
|
+
ids: p.node_id,
|
|
482
|
+
format: 'svg',
|
|
483
|
+
svg_outline_text: p.svg_outline_text,
|
|
484
|
+
})
|
|
485
|
+
)
|
|
486
|
+
),
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'export_node_as_pdf',
|
|
490
|
+
description:
|
|
491
|
+
'Exports a Figma frame as a PDF document. Use for design specifications, presentations or print-ready assets.',
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: 'object',
|
|
494
|
+
properties: {
|
|
495
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
496
|
+
node_id: { type: 'string', description: 'Node id' },
|
|
497
|
+
},
|
|
498
|
+
required: ['file_key', 'node_id'],
|
|
499
|
+
},
|
|
500
|
+
handler: async (p) =>
|
|
501
|
+
text(
|
|
502
|
+
await apiRequest('GET', '/v1/images/{file_key}', {
|
|
503
|
+
file_key: p.file_key,
|
|
504
|
+
ids: p.node_id,
|
|
505
|
+
format: 'pdf',
|
|
506
|
+
})
|
|
507
|
+
),
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'export_multiple_nodes',
|
|
511
|
+
description:
|
|
512
|
+
'Exports multiple Figma nodes in one API call. More efficient than individual exports when you need many assets at once. Returns a map of node IDs to download URLs.',
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
517
|
+
node_ids: { type: 'array', items: { type: 'string' }, description: 'Node IDs to render' },
|
|
518
|
+
format: {
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'png, svg, pdf, or jpg',
|
|
521
|
+
enum: ['png', 'svg', 'pdf', 'jpg'],
|
|
522
|
+
default: 'png',
|
|
523
|
+
},
|
|
524
|
+
scale: { type: 'number', description: 'Image scale (default 1)', default: 1 },
|
|
525
|
+
},
|
|
526
|
+
required: ['file_key', 'node_ids'],
|
|
527
|
+
},
|
|
528
|
+
handler: async (p) =>
|
|
529
|
+
text(
|
|
530
|
+
await apiRequest(
|
|
531
|
+
'GET',
|
|
532
|
+
'/v1/images/{file_key}',
|
|
533
|
+
scrub({
|
|
534
|
+
file_key: p.file_key,
|
|
535
|
+
ids: p.node_ids.join(','),
|
|
536
|
+
format: p.format ?? 'png',
|
|
537
|
+
scale: p.scale ?? 1,
|
|
538
|
+
})
|
|
539
|
+
)
|
|
540
|
+
),
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: 'get_file_comments',
|
|
544
|
+
description:
|
|
545
|
+
'Retrieves all comments on a Figma file with author, content, position and resolved status. Use to review design feedback before a design review meeting.',
|
|
546
|
+
inputSchema: {
|
|
547
|
+
type: 'object',
|
|
548
|
+
properties: {
|
|
549
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
550
|
+
as_md: { type: 'boolean', description: 'Return comments as markdown when applicable' },
|
|
551
|
+
},
|
|
552
|
+
required: ['file_key'],
|
|
553
|
+
},
|
|
554
|
+
handler: async (p) =>
|
|
555
|
+
text(await apiRequest('GET', '/v1/files/{file_key}/comments', scrub({ file_key: p.file_key, as_md: p.as_md }))),
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: 'add_comment',
|
|
559
|
+
description:
|
|
560
|
+
'Posts a new comment on a Figma file. Can be placed at a specific canvas position or attached to a node. Use to give design feedback directly in Figma.',
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: 'object',
|
|
563
|
+
properties: {
|
|
564
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
565
|
+
message: { type: 'string', description: 'Comment text' },
|
|
566
|
+
client_meta: { type: 'object', description: 'Position: Vector, Region, FrameOffset, or FrameOffsetRegion' },
|
|
567
|
+
comment_id: { type: 'string', description: 'Parent comment id for replies' },
|
|
568
|
+
_rawBody: { type: 'object', description: 'Full POST body; overrides message/client_meta/comment_id when set' },
|
|
569
|
+
},
|
|
570
|
+
required: ['file_key', 'message'],
|
|
571
|
+
},
|
|
572
|
+
handler: async (p) => {
|
|
573
|
+
if (p._rawBody && typeof p._rawBody === 'object') {
|
|
574
|
+
return text(
|
|
575
|
+
await apiRequest('POST', '/v1/files/{file_key}/comments', { file_key: p.file_key, _rawBody: p._rawBody })
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return text(
|
|
579
|
+
await apiRequest('POST', '/v1/files/{file_key}/comments', {
|
|
580
|
+
file_key: p.file_key,
|
|
581
|
+
_rawBody: scrub({ message: p.message, client_meta: p.client_meta, comment_id: p.comment_id }),
|
|
582
|
+
})
|
|
583
|
+
);
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'resolve_comment',
|
|
588
|
+
description:
|
|
589
|
+
"Marks a Figma comment as resolved. Use after addressing design feedback to keep the file's comment thread clean. Note: Official docs list GET/POST/DELETE for comments only; this calls PUT { resolved: true } as specified and may 404 if unsupported.",
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {
|
|
593
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
594
|
+
comment_id: { type: 'string', description: 'Comment id' },
|
|
595
|
+
},
|
|
596
|
+
required: ['file_key', 'comment_id'],
|
|
597
|
+
},
|
|
598
|
+
handler: async (p) =>
|
|
599
|
+
text(
|
|
600
|
+
await apiRequest('PUT', '/v1/files/{file_key}/comments/{comment_id}', {
|
|
601
|
+
file_key: p.file_key,
|
|
602
|
+
comment_id: p.comment_id,
|
|
603
|
+
_rawBody: { resolved: true },
|
|
604
|
+
})
|
|
605
|
+
),
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'delete_comment',
|
|
609
|
+
description:
|
|
610
|
+
'Permanently deletes a comment from a Figma file. Only the comment author can delete their own comments.',
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
615
|
+
comment_id: { type: 'string', description: 'Comment id' },
|
|
616
|
+
},
|
|
617
|
+
required: ['file_key', 'comment_id'],
|
|
618
|
+
},
|
|
619
|
+
handler: async (p) =>
|
|
620
|
+
text(
|
|
621
|
+
await apiRequest('DELETE', '/v1/files/{file_key}/comments/{comment_id}', {
|
|
622
|
+
file_key: p.file_key,
|
|
623
|
+
comment_id: p.comment_id,
|
|
624
|
+
})
|
|
625
|
+
),
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: 'get_team_components',
|
|
629
|
+
description:
|
|
630
|
+
'Lists all published components in a Figma team library with names, keys and descriptions. Use to browse available design system components.',
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: 'object',
|
|
633
|
+
properties: {
|
|
634
|
+
team_id: { type: 'string', description: 'Team id' },
|
|
635
|
+
page_size: { type: 'number', description: 'Page size (default 30, max 1000)', default: 30 },
|
|
636
|
+
cursor: { type: 'number', description: 'Maps to Figma after cursor' },
|
|
637
|
+
after: { type: 'number', description: 'After cursor' },
|
|
638
|
+
before: { type: 'number', description: 'Before cursor' },
|
|
639
|
+
},
|
|
640
|
+
required: ['team_id'],
|
|
641
|
+
},
|
|
642
|
+
handler: async (p) =>
|
|
643
|
+
text(
|
|
644
|
+
await apiRequest('GET', '/v1/teams/{team_id}/components', scrub({
|
|
645
|
+
team_id: p.team_id,
|
|
646
|
+
page_size: p.page_size ?? 30,
|
|
647
|
+
after: p.cursor ?? p.after,
|
|
648
|
+
before: p.before,
|
|
649
|
+
}))
|
|
650
|
+
),
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: 'get_component',
|
|
654
|
+
description:
|
|
655
|
+
'Retrieves detailed metadata for a single published Figma component including its node ID, description and containing file. Use after get_team_components to inspect a specific component.',
|
|
656
|
+
inputSchema: {
|
|
657
|
+
type: 'object',
|
|
658
|
+
properties: { key: { type: 'string', description: 'Published component key' } },
|
|
659
|
+
required: ['key'],
|
|
660
|
+
},
|
|
661
|
+
handler: async (p) => text(await apiRequest('GET', '/v1/components/{key}', { key: p.key })),
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: 'get_team_component_sets',
|
|
665
|
+
description:
|
|
666
|
+
'Lists all component sets (variant groups) in a Figma team library. Use to discover component variants before implementing a design system.',
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: 'object',
|
|
669
|
+
properties: {
|
|
670
|
+
team_id: { type: 'string', description: 'Team id' },
|
|
671
|
+
page_size: { type: 'number', description: 'Page size (default 30)', default: 30 },
|
|
672
|
+
cursor: { type: 'number', description: 'Maps to Figma after cursor' },
|
|
673
|
+
after: { type: 'number' },
|
|
674
|
+
before: { type: 'number' },
|
|
675
|
+
},
|
|
676
|
+
required: ['team_id'],
|
|
677
|
+
},
|
|
678
|
+
handler: async (p) =>
|
|
679
|
+
text(
|
|
680
|
+
await apiRequest('GET', '/v1/teams/{team_id}/component_sets', scrub({
|
|
681
|
+
team_id: p.team_id,
|
|
682
|
+
page_size: p.page_size ?? 30,
|
|
683
|
+
after: p.cursor ?? p.after,
|
|
684
|
+
before: p.before,
|
|
685
|
+
}))
|
|
686
|
+
),
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: 'get_team_styles',
|
|
690
|
+
description:
|
|
691
|
+
'Lists all published styles (colors, text, effects, grids) in a Figma team library. Use to extract design tokens like brand colors and typography from the design system.',
|
|
692
|
+
inputSchema: {
|
|
693
|
+
type: 'object',
|
|
694
|
+
properties: {
|
|
695
|
+
team_id: { type: 'string', description: 'Team id' },
|
|
696
|
+
page_size: { type: 'number', description: 'Page size (default 30)', default: 30 },
|
|
697
|
+
cursor: { type: 'number', description: 'Maps to Figma after cursor' },
|
|
698
|
+
after: { type: 'number' },
|
|
699
|
+
before: { type: 'number' },
|
|
700
|
+
},
|
|
701
|
+
required: ['team_id'],
|
|
702
|
+
},
|
|
703
|
+
handler: async (p) =>
|
|
704
|
+
text(
|
|
705
|
+
await apiRequest('GET', '/v1/teams/{team_id}/styles', scrub({
|
|
706
|
+
team_id: p.team_id,
|
|
707
|
+
page_size: p.page_size ?? 30,
|
|
708
|
+
after: p.cursor ?? p.after,
|
|
709
|
+
before: p.before,
|
|
710
|
+
}))
|
|
711
|
+
),
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: 'get_local_variables',
|
|
715
|
+
description:
|
|
716
|
+
'Retrieves all local variables defined in a Figma file including their names, types, values and scopes. Use to extract design tokens for design system implementation.',
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: 'object',
|
|
719
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
720
|
+
required: ['file_key'],
|
|
721
|
+
},
|
|
722
|
+
handler: async (p) =>
|
|
723
|
+
text(await apiRequest('GET', '/v1/files/{file_key}/variables/local', { file_key: p.file_key })),
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: 'get_published_variables',
|
|
727
|
+
description:
|
|
728
|
+
'Retrieves all variables published from a Figma file for use in other files. Use to get the canonical design token values that other files reference.',
|
|
729
|
+
inputSchema: {
|
|
730
|
+
type: 'object',
|
|
731
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
732
|
+
required: ['file_key'],
|
|
733
|
+
},
|
|
734
|
+
handler: async (p) =>
|
|
735
|
+
text(await apiRequest('GET', '/v1/files/{file_key}/variables/published', { file_key: p.file_key })),
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: 'create_variables',
|
|
739
|
+
description:
|
|
740
|
+
'Creates new variables (design tokens) in a Figma file. Supports creating variable collections, modes and values in one batch call. Use to programmatically build or update a design system.',
|
|
741
|
+
inputSchema: {
|
|
742
|
+
type: 'object',
|
|
743
|
+
properties: {
|
|
744
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
745
|
+
variables: { type: 'array', description: 'VariableChange[] (create actions)' },
|
|
746
|
+
collections: { type: 'array', description: 'Maps to variableCollections' },
|
|
747
|
+
modes: { type: 'array', description: 'Maps to variableModes' },
|
|
748
|
+
variableModeValues: { type: 'array', description: 'Maps to variableModeValues' },
|
|
749
|
+
_rawBody: { type: 'object', description: 'Full POST body for POST /variables' },
|
|
750
|
+
},
|
|
751
|
+
required: ['file_key'],
|
|
752
|
+
},
|
|
753
|
+
handler: async (p) => {
|
|
754
|
+
const body =
|
|
755
|
+
p._rawBody ||
|
|
756
|
+
scrub({
|
|
757
|
+
variableCollections: p.collections,
|
|
758
|
+
variableModes: p.modes,
|
|
759
|
+
variables: p.variables,
|
|
760
|
+
variableModeValues: p.variableModeValues,
|
|
761
|
+
});
|
|
762
|
+
return text(
|
|
763
|
+
await apiRequest('POST', '/v1/files/{file_key}/variables', { file_key: p.file_key, _rawBody: body })
|
|
764
|
+
);
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: 'update_variables',
|
|
769
|
+
description:
|
|
770
|
+
'Updates existing variables in a Figma file including their values, scopes and modes. Use to sync design tokens from a code source of truth back into Figma.',
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: 'object',
|
|
773
|
+
properties: {
|
|
774
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
775
|
+
variables: { type: 'array', description: 'VariableChange[] update/delete actions' },
|
|
776
|
+
variableCollections: { type: 'array', description: 'Optional collection updates' },
|
|
777
|
+
variableModes: { type: 'array', description: 'Optional mode updates' },
|
|
778
|
+
variableModeValues: { type: 'array', description: 'Optional mode value updates' },
|
|
779
|
+
_rawBody: { type: 'object', description: 'Full POST body for POST /variables' },
|
|
780
|
+
},
|
|
781
|
+
required: ['file_key', 'variables'],
|
|
782
|
+
},
|
|
783
|
+
handler: async (p) => {
|
|
784
|
+
const body =
|
|
785
|
+
p._rawBody ||
|
|
786
|
+
scrub({
|
|
787
|
+
variables: p.variables,
|
|
788
|
+
variableCollections: p.variableCollections,
|
|
789
|
+
variableModes: p.variableModes,
|
|
790
|
+
variableModeValues: p.variableModeValues,
|
|
791
|
+
});
|
|
792
|
+
return text(
|
|
793
|
+
await apiRequest('POST', '/v1/files/{file_key}/variables', { file_key: p.file_key, _rawBody: body })
|
|
794
|
+
);
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
];
|
|
798
|
+
|
|
799
|
+
const COMPOSITE_TOOLS = [
|
|
800
|
+
{
|
|
801
|
+
name: 'discover_figma_resources',
|
|
802
|
+
description:
|
|
803
|
+
'Universal Figma resource discovery. Parses any figma.com URL for file_key, node_id, team_id, project_id; optionally fetches shallow file (depth=1) and team projects.',
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: 'object',
|
|
806
|
+
properties: {
|
|
807
|
+
figma_url: { type: 'string', description: 'figma.com file/design/proto/team/project URL' },
|
|
808
|
+
fetch_children: { type: 'boolean', description: 'Fetch related API payloads', default: true },
|
|
809
|
+
},
|
|
810
|
+
required: ['figma_url'],
|
|
811
|
+
},
|
|
812
|
+
handler: async (p) => {
|
|
813
|
+
const parsed = parseFigmaUrl(p.figma_url);
|
|
814
|
+
const result = { parsed, fetched: {} };
|
|
815
|
+
if (p.fetch_children === false) return text(result);
|
|
816
|
+
if (parsed.file_key) {
|
|
817
|
+
result.fetched.file_pages = await apiRequest('GET', '/v1/files/{file_key}', {
|
|
818
|
+
file_key: parsed.file_key,
|
|
819
|
+
depth: 1,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (parsed.team_id) {
|
|
823
|
+
try {
|
|
824
|
+
result.fetched.team_projects = await apiRequest('GET', '/v1/teams/{team_id}/projects', {
|
|
825
|
+
team_id: parsed.team_id,
|
|
826
|
+
});
|
|
827
|
+
} catch (e) {
|
|
828
|
+
result.fetched.team_projects_error = e.message;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (parsed.project_id) {
|
|
832
|
+
try {
|
|
833
|
+
result.fetched.project_files = await apiRequest('GET', '/v1/projects/{project_id}/files', {
|
|
834
|
+
project_id: parsed.project_id,
|
|
835
|
+
});
|
|
836
|
+
} catch (e) {
|
|
837
|
+
result.fetched.project_files_error = e.message;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return text(result);
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'extract_design_tokens',
|
|
845
|
+
description:
|
|
846
|
+
'Extracts design tokens from a file: local + published variables, file styles, file components. token_format is a hint for consumers.',
|
|
847
|
+
inputSchema: {
|
|
848
|
+
type: 'object',
|
|
849
|
+
properties: {
|
|
850
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
851
|
+
token_format: {
|
|
852
|
+
type: 'string',
|
|
853
|
+
enum: ['css', 'js', 'json', 'tailwind'],
|
|
854
|
+
default: 'json',
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
required: ['file_key'],
|
|
858
|
+
},
|
|
859
|
+
handler: async (p) => text(await extractTokensPayload(p.file_key, p.token_format || 'json')),
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
name: 'convert_tokens_to_tailwind',
|
|
863
|
+
description:
|
|
864
|
+
'Runs extract_design_tokens and builds a Tailwind-style theme.extend object (best-effort from style metadata).',
|
|
865
|
+
inputSchema: {
|
|
866
|
+
type: 'object',
|
|
867
|
+
properties: {
|
|
868
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
869
|
+
include_font_imports: { type: 'boolean', default: true },
|
|
870
|
+
},
|
|
871
|
+
required: ['file_key'],
|
|
872
|
+
},
|
|
873
|
+
handler: async (p) => {
|
|
874
|
+
const base = await extractTokensPayload(p.file_key, 'tailwind');
|
|
875
|
+
const tailwindConfig = { theme: buildTailwindThemeFromTokens(base) };
|
|
876
|
+
if (p.include_font_imports !== false) tailwindConfig.fontImports = [];
|
|
877
|
+
return text({ source: base.meta, tailwindConfig, raw: base });
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: 'convert_tokens_to_css_variables',
|
|
882
|
+
description:
|
|
883
|
+
'Extracts tokens and emits a :root { } CSS snippet with placeholder custom properties (best-effort from variable metadata).',
|
|
884
|
+
inputSchema: {
|
|
885
|
+
type: 'object',
|
|
886
|
+
properties: {
|
|
887
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
888
|
+
prefix: { type: 'string', default: '--figma' },
|
|
889
|
+
},
|
|
890
|
+
required: ['file_key'],
|
|
891
|
+
},
|
|
892
|
+
handler: async (p) => {
|
|
893
|
+
const data = await extractTokensPayload(p.file_key, 'css');
|
|
894
|
+
const prefix = (p.prefix || '--figma').replace(/\s+/g, '');
|
|
895
|
+
const lines = [':root {'];
|
|
896
|
+
const vars = data.localVariables?.meta?.variables || {};
|
|
897
|
+
for (const [id, v] of Object.entries(vars)) {
|
|
898
|
+
const name = `${prefix}-${String(v.name || id)
|
|
899
|
+
.replace(/\s+/g, '-')
|
|
900
|
+
.toLowerCase()}`;
|
|
901
|
+
lines.push(` ${name}: /* ${v.resolvedType || 'value'} */ var(--figma-var-${id.slice(0, 8)});`);
|
|
902
|
+
}
|
|
903
|
+
lines.push('}');
|
|
904
|
+
return text({ css: lines.join('\n'), structured: data });
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: 'get_file_full_structure',
|
|
909
|
+
description:
|
|
910
|
+
'Pages at depth=1, then per-page subtree at depth=3 via ids filter for navigable outline.',
|
|
911
|
+
inputSchema: {
|
|
912
|
+
type: 'object',
|
|
913
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
914
|
+
required: ['file_key'],
|
|
915
|
+
},
|
|
916
|
+
handler: async (p) => {
|
|
917
|
+
const file = await apiRequest('GET', '/v1/files/{file_key}', { file_key: p.file_key, depth: 1 });
|
|
918
|
+
const pages = file.document?.children || [];
|
|
919
|
+
const outline = [];
|
|
920
|
+
for (const page of pages) {
|
|
921
|
+
const sub = await apiRequest('GET', '/v1/files/{file_key}', {
|
|
922
|
+
file_key: p.file_key,
|
|
923
|
+
depth: 3,
|
|
924
|
+
ids: page.id,
|
|
925
|
+
});
|
|
926
|
+
outline.push({
|
|
927
|
+
page: { id: page.id, name: page.name },
|
|
928
|
+
subtree: sub.document?.children || sub.nodes || sub,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return text({ name: file.name, role: file.role, outline });
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
name: 'get_node_with_children',
|
|
936
|
+
description:
|
|
937
|
+
'Fetches a node via GET nodes, then walks response for child ids and issues follow-up GET nodes batches up to max_depth rounds.',
|
|
938
|
+
inputSchema: {
|
|
939
|
+
type: 'object',
|
|
940
|
+
properties: {
|
|
941
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
942
|
+
node_id: { type: 'string', description: 'Root node id' },
|
|
943
|
+
max_depth: { type: 'number', description: 'BFS rounds (default 5)', default: 5 },
|
|
944
|
+
},
|
|
945
|
+
required: ['file_key', 'node_id'],
|
|
946
|
+
},
|
|
947
|
+
handler: async (p) => {
|
|
948
|
+
const maxDepth = p.max_depth ?? 5;
|
|
949
|
+
let frontier = new Set([p.node_id]);
|
|
950
|
+
const seen = new Set();
|
|
951
|
+
const chunks = [];
|
|
952
|
+
let rounds = 0;
|
|
953
|
+
while (frontier.size && rounds < maxDepth) {
|
|
954
|
+
const ids = [...frontier].filter((id) => !seen.has(id));
|
|
955
|
+
if (!ids.length) break;
|
|
956
|
+
for (const id of ids) seen.add(id);
|
|
957
|
+
const part = await apiRequest('GET', '/v1/files/{file_key}/nodes', {
|
|
958
|
+
file_key: p.file_key,
|
|
959
|
+
ids: ids.join(','),
|
|
960
|
+
});
|
|
961
|
+
chunks.push(part);
|
|
962
|
+
const next = new Set();
|
|
963
|
+
const nodesObj = part.nodes || {};
|
|
964
|
+
for (const doc of Object.values(nodesObj)) {
|
|
965
|
+
walkNodes(doc.document, (n) => {
|
|
966
|
+
if (n.id && !seen.has(n.id)) next.add(n.id);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
frontier = next;
|
|
970
|
+
rounds += 1;
|
|
971
|
+
}
|
|
972
|
+
return text({ node_id: p.node_id, rounds, merged_chunks: chunks });
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: 'export_design_spec',
|
|
977
|
+
description:
|
|
978
|
+
'Bundles GET nodes, file styles, comments, and meta for a node_id; returns markdown or JSON.',
|
|
979
|
+
inputSchema: {
|
|
980
|
+
type: 'object',
|
|
981
|
+
properties: {
|
|
982
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
983
|
+
node_id: { type: 'string', description: 'Target node id' },
|
|
984
|
+
format: { type: 'string', enum: ['md', 'json'], default: 'md' },
|
|
985
|
+
},
|
|
986
|
+
required: ['file_key', 'node_id'],
|
|
987
|
+
},
|
|
988
|
+
handler: async (p) => {
|
|
989
|
+
const [nodes, styles, comments, meta] = await Promise.all([
|
|
990
|
+
apiRequest('GET', '/v1/files/{file_key}/nodes', { file_key: p.file_key, ids: p.node_id }),
|
|
991
|
+
apiRequest('GET', '/v1/files/{file_key}/styles', { file_key: p.file_key }),
|
|
992
|
+
apiRequest('GET', '/v1/files/{file_key}/comments', { file_key: p.file_key }),
|
|
993
|
+
apiRequest('GET', '/v1/files/{file_key}/meta', { file_key: p.file_key }),
|
|
994
|
+
]);
|
|
995
|
+
const spec = { nodes, styles, comments, meta };
|
|
996
|
+
if (p.format === 'json') return text(spec);
|
|
997
|
+
const lines = [
|
|
998
|
+
'# Design spec',
|
|
999
|
+
'',
|
|
1000
|
+
`File: ${meta.name || p.file_key} · Node: \`${p.node_id}\``,
|
|
1001
|
+
'',
|
|
1002
|
+
'## Node JSON',
|
|
1003
|
+
'```json',
|
|
1004
|
+
JSON.stringify(nodes, null, 2).slice(0, 12000),
|
|
1005
|
+
'```',
|
|
1006
|
+
'',
|
|
1007
|
+
'## Comments (first 20)',
|
|
1008
|
+
JSON.stringify((comments.comments || []).slice(0, 20), null, 2),
|
|
1009
|
+
];
|
|
1010
|
+
return text({ format: 'md', markdown: lines.join('\n'), raw: spec });
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: 'search_nodes_by_name',
|
|
1015
|
+
description:
|
|
1016
|
+
'GET full file document and walk the tree; supports * ? wildcards and optional node_type filter.',
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: 'object',
|
|
1019
|
+
properties: {
|
|
1020
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
1021
|
+
name_query: { type: 'string', description: 'Pattern (* ?)' },
|
|
1022
|
+
node_type: { type: 'string', description: 'FRAME, COMPONENT, TEXT, …' },
|
|
1023
|
+
},
|
|
1024
|
+
required: ['file_key', 'name_query'],
|
|
1025
|
+
},
|
|
1026
|
+
handler: async (p) => {
|
|
1027
|
+
const file = await apiRequest('GET', '/v1/files/{file_key}', { file_key: p.file_key });
|
|
1028
|
+
const hits = [];
|
|
1029
|
+
walkNodes(file.document, (node, pathNames) => {
|
|
1030
|
+
const okName = nameMatch(p.name_query, node.name || '');
|
|
1031
|
+
const okType = !p.node_type || node.type === p.node_type;
|
|
1032
|
+
if (okName && okType) {
|
|
1033
|
+
hits.push({
|
|
1034
|
+
id: node.id,
|
|
1035
|
+
name: node.name,
|
|
1036
|
+
type: node.type,
|
|
1037
|
+
path: pathNames.filter(Boolean).join(' / '),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
return text({ count: hits.length, hits: hits.slice(0, 200) });
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: 'get_prototype_flows',
|
|
1046
|
+
description:
|
|
1047
|
+
'No dedicated /prototype/flows route; aggregates flowStartingPoints from file JSON (depth=2).',
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
type: 'object',
|
|
1050
|
+
properties: { file_key: { type: 'string', description: 'Figma file key' } },
|
|
1051
|
+
required: ['file_key'],
|
|
1052
|
+
},
|
|
1053
|
+
handler: async (p) => {
|
|
1054
|
+
const file = await apiRequest('GET', '/v1/files/{file_key}', { file_key: p.file_key, depth: 2 });
|
|
1055
|
+
const flows = [];
|
|
1056
|
+
walkNodes(file.document, (node) => {
|
|
1057
|
+
if (node.flowStartingPoints?.length) {
|
|
1058
|
+
flows.push(
|
|
1059
|
+
...node.flowStartingPoints.map((f) => ({
|
|
1060
|
+
...f,
|
|
1061
|
+
pageOrNodeId: node.id,
|
|
1062
|
+
pageOrNodeName: node.name,
|
|
1063
|
+
}))
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
return text({
|
|
1068
|
+
fileName: file.name,
|
|
1069
|
+
prototypeStartingPoint: file.prototypeStartingPoint,
|
|
1070
|
+
flowStartingPoints: flows,
|
|
1071
|
+
});
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
name: 'compare_file_versions',
|
|
1076
|
+
description:
|
|
1077
|
+
'GET file twice with different version ids (depth=4) and diff node id sets (added / removed / common counts).',
|
|
1078
|
+
inputSchema: {
|
|
1079
|
+
type: 'object',
|
|
1080
|
+
properties: {
|
|
1081
|
+
file_key: { type: 'string', description: 'Figma file key' },
|
|
1082
|
+
version_id_1: { type: 'string', description: 'First version id' },
|
|
1083
|
+
version_id_2: { type: 'string', description: 'Second version id' },
|
|
1084
|
+
},
|
|
1085
|
+
required: ['file_key', 'version_id_1', 'version_id_2'],
|
|
1086
|
+
},
|
|
1087
|
+
handler: async (p) => {
|
|
1088
|
+
const [a, b] = await Promise.all([
|
|
1089
|
+
apiRequest('GET', '/v1/files/{file_key}', {
|
|
1090
|
+
file_key: p.file_key,
|
|
1091
|
+
version: p.version_id_1,
|
|
1092
|
+
depth: 4,
|
|
1093
|
+
}),
|
|
1094
|
+
apiRequest('GET', '/v1/files/{file_key}', {
|
|
1095
|
+
file_key: p.file_key,
|
|
1096
|
+
version: p.version_id_2,
|
|
1097
|
+
depth: 4,
|
|
1098
|
+
}),
|
|
1099
|
+
]);
|
|
1100
|
+
const idsA = new Set();
|
|
1101
|
+
const idsB = new Set();
|
|
1102
|
+
walkNodes(a.document, (n) => {
|
|
1103
|
+
if (n.id) idsA.add(n.id);
|
|
1104
|
+
});
|
|
1105
|
+
walkNodes(b.document, (n) => {
|
|
1106
|
+
if (n.id) idsB.add(n.id);
|
|
1107
|
+
});
|
|
1108
|
+
const added = [...idsB].filter((id) => !idsA.has(id));
|
|
1109
|
+
const removed = [...idsA].filter((id) => !idsB.has(id));
|
|
1110
|
+
const common = [...idsA].filter((id) => idsB.has(id));
|
|
1111
|
+
return text({
|
|
1112
|
+
version_id_1: p.version_id_1,
|
|
1113
|
+
version_id_2: p.version_id_2,
|
|
1114
|
+
summary: { added_count: added.length, removed_count: removed.length, common_count: common.length },
|
|
1115
|
+
sample_added: added.slice(0, 50),
|
|
1116
|
+
sample_removed: removed.slice(0, 50),
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
];
|
|
1121
|
+
|
|
1122
|
+
function dedupeToolNames(list) {
|
|
1123
|
+
const seen = new Map();
|
|
1124
|
+
return list.map((t) => {
|
|
1125
|
+
let name = t.name;
|
|
1126
|
+
if (seen.has(name)) {
|
|
1127
|
+
let n = 2;
|
|
1128
|
+
while (seen.has(`${name}_v${n}`)) n += 1;
|
|
1129
|
+
name = `${name}_v${n}`;
|
|
1130
|
+
}
|
|
1131
|
+
seen.set(name, true);
|
|
1132
|
+
return { ...t, name };
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const generated = buildGeneratedTools();
|
|
1137
|
+
export const tools = dedupeToolNames([...CUSTOM_TOOLS_FRONT, ...generated, ...COMPOSITE_TOOLS]);
|