import { getIntlayerAPI } from '@intlayer/api'; import { editor } from '@intlayer/config/built'; import { getEditorClientSecret } from '@intlayer/config/secrets'; import type { ContentNode } from '@intlayer/types/dictionary'; import { z } from 'zod/mini'; import type { McpServer } from './docs'; type LoadAPITools = (server: McpServer) => void; const authSchema = { clientId: z.optional(z.string()).register(z.globalRegistry, { description: 'Intlayer OAuth2 client ID (access key). Falls back to INTLAYER_CLIENT_ID env var.', }), clientSecret: z.optional(z.string()).register(z.globalRegistry, { description: 'Intlayer OAuth2 client secret. Falls back to INTLAYER_CLIENT_SECRET env var.', }), }; const getAPI = async (clientId?: string, clientSecret?: string) => { const resolvedClientId = clientId ?? editor.clientId; const resolvedClientSecret = clientSecret ?? getEditorClientSecret(); if (!resolvedClientId || !resolvedClientSecret) { throw new Error( 'Intlayer credentials not found. Provide clientId/clientSecret or set INTLAYER_CLIENT_ID/INTLAYER_CLIENT_SECRET.' ); } const configWithCreds = { editor: { ...editor, clientId: resolvedClientId, clientSecret: resolvedClientSecret, }, }; const tempAPI = getIntlayerAPI({}, configWithCreds); const tokenResult = await tempAPI.oAuth.getOAuth2AccessToken(); const token = (tokenResult as any)?.data?.access_token as string | undefined; if (!token) { throw new Error( 'Failed to obtain OAuth2 access token. Check your credentials.' ); } return getIntlayerAPI( { headers: { Authorization: `Bearer ${token}` } }, { editor } ); }; const ok = (data: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], }); const fail = (label: string, error: unknown) => ({ content: [ { type: 'text' as const, text: `${label} failed: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true as const, }); export const loadAPITools: LoadAPITools = (server) => { // ── Dictionaries ────────────────────────────────────────────────────────── server.registerTool( 'intlayer-dictionaries-list', { title: 'List Dictionaries', description: 'List all dictionaries for the selected project. Returns keys, IDs, and metadata.', inputSchema: { ...authSchema, page: z .optional(z.number()) .register(z.globalRegistry, { description: 'Page number (1-based)' }), pageSize: z .optional(z.number()) .register(z.globalRegistry, { description: 'Items per page' }), }, annotations: { readOnlyHint: true }, }, async ({ clientId, clientSecret, page, pageSize }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.dictionary.getDictionaries({ page, pageSize, }); return ok(result); } catch (error) { return fail('List dictionaries', error); } } ); server.registerTool( 'intlayer-dictionary-get', { title: 'Get Dictionary', description: 'Get a dictionary by its key, including its full content.', inputSchema: { ...authSchema, dictionaryKey: z .string() .register(z.globalRegistry, { description: 'The dictionary key' }), }, annotations: { readOnlyHint: true }, }, async ({ clientId, clientSecret, dictionaryKey }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.dictionary.getDictionary(dictionaryKey); return ok(result); } catch (error) { return fail('Get dictionary', error); } } ); server.registerTool( 'intlayer-dictionary-create', { title: 'Create Dictionary', description: 'Create a new dictionary in the selected project.', inputSchema: { ...authSchema, projectId: z.string().register(z.globalRegistry, { description: 'ID of the CMS project owning the dictionary. Must be the currently selected project.', }), key: z.string().register(z.globalRegistry, { description: 'Unique key for the dictionary', }), title: z .optional(z.string()) .register(z.globalRegistry, { description: 'Human-readable title' }), description: z.optional(z.string()).register(z.globalRegistry, { description: 'Description of the dictionary', }), content: z .optional(z.record(z.string(), z.unknown())) .register(z.globalRegistry, { description: 'Initial content as JSON object', }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, projectId, key, title, description, content, }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.dictionary.addDictionary({ dictionary: { key, title, description, content: content as ContentNode | undefined, projectIds: [projectId], }, }); return ok(result); } catch (error) { return fail('Create dictionary', error); } } ); server.registerTool( 'intlayer-dictionary-update', { title: 'Update Dictionary', description: 'Update an existing dictionary content or metadata.', inputSchema: { ...authSchema, id: z .string() .register(z.globalRegistry, { description: 'Dictionary ID' }), key: z.optional(z.string()).register(z.globalRegistry, { description: 'New key for the dictionary', }), title: z .optional(z.string()) .register(z.globalRegistry, { description: 'New title' }), description: z .optional(z.string()) .register(z.globalRegistry, { description: 'New description' }), content: z .optional(z.record(z.string(), z.unknown())) .register(z.globalRegistry, { description: 'Updated content as JSON object', }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, id, key, title, description, content, }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.dictionary.updateDictionary({ id, key, title, description, content, }); return ok(result); } catch (error) { return fail('Update dictionary', error); } } ); server.registerTool( 'intlayer-dictionary-delete', { title: 'Delete Dictionary', description: 'Delete a dictionary by its ID. This action is irreversible.', inputSchema: { ...authSchema, dictionaryId: z.string().register(z.globalRegistry, { description: 'Dictionary ID to delete', }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, dictionaryId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.dictionary.deleteDictionary(dictionaryId); return ok(result); } catch (error) { return fail('Delete dictionary', error); } } ); // ── Tags ────────────────────────────────────────────────────────────────── server.registerTool( 'intlayer-tags-list', { title: 'List Tags', description: 'List all tags for the selected organization.', inputSchema: { ...authSchema, page: z .optional(z.number()) .register(z.globalRegistry, { description: 'Page number (1-based)' }), pageSize: z .optional(z.number()) .register(z.globalRegistry, { description: 'Items per page' }), }, annotations: { readOnlyHint: true }, }, async ({ clientId, clientSecret, page, pageSize }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.tag.getTags({ page, pageSize }); return ok(result); } catch (error) { return fail('List tags', error); } } ); server.registerTool( 'intlayer-tag-create', { title: 'Create Tag', description: 'Create a new tag in the organization. Tags can be used to group dictionaries and provide AI context.', inputSchema: { ...authSchema, key: z .string() .register(z.globalRegistry, { description: 'Unique tag key' }), name: z.optional(z.string()).register(z.globalRegistry, { description: 'Display name for the tag', }), description: z.optional(z.string()).register(z.globalRegistry, { description: 'Description of the tag', }), instructions: z.optional(z.string()).register(z.globalRegistry, { description: 'AI instructions to apply when this tag is used', }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, key, name, description, instructions, }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.tag.addTag({ key, name, description, instructions, }); return ok(result); } catch (error) { return fail('Create tag', error); } } ); server.registerTool( 'intlayer-tag-update', { title: 'Update Tag', description: 'Update an existing tag.', inputSchema: { ...authSchema, tagId: z .string() .register(z.globalRegistry, { description: 'Tag ID to update' }), key: z .optional(z.string()) .register(z.globalRegistry, { description: 'New key' }), name: z .optional(z.string()) .register(z.globalRegistry, { description: 'New display name' }), description: z .optional(z.string()) .register(z.globalRegistry, { description: 'New description' }), instructions: z .optional(z.string()) .register(z.globalRegistry, { description: 'New AI instructions' }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, tagId, key, name, description, instructions, }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.tag.updateTag(tagId, { key, name, description, instructions, }); return ok(result); } catch (error) { return fail('Update tag', error); } } ); server.registerTool( 'intlayer-tag-delete', { title: 'Delete Tag', description: 'Delete a tag by its ID.', inputSchema: { ...authSchema, tagId: z .string() .register(z.globalRegistry, { description: 'Tag ID to delete' }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, tagId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.tag.deleteTag(tagId); return ok(result); } catch (error) { return fail('Delete tag', error); } } ); // ── Organizations ───────────────────────────────────────────────────────── server.registerTool( 'intlayer-organizations-list', { title: 'List Organizations', description: 'List all organizations the authenticated user belongs to.', inputSchema: { ...authSchema, page: z .optional(z.number()) .register(z.globalRegistry, { description: 'Page number (1-based)' }), pageSize: z .optional(z.number()) .register(z.globalRegistry, { description: 'Items per page' }), }, annotations: { readOnlyHint: true }, }, async ({ clientId, clientSecret, page, pageSize }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.organization.getOrganizations({ page, pageSize, }); return ok(result); } catch (error) { return fail('List organizations', error); } } ); server.registerTool( 'intlayer-organization-select', { title: 'Select Organization', description: 'Select an organization as the current active organization. Required before accessing organization-specific resources.', inputSchema: { ...authSchema, organizationId: z.string().register(z.globalRegistry, { description: 'Organization ID to select', }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, organizationId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.organization.selectOrganization(organizationId); return ok(result); } catch (error) { return fail('Select organization', error); } } ); server.registerTool( 'intlayer-organization-update', { title: 'Update Organization', description: 'Update the selected organization name or settings.', inputSchema: { ...authSchema, name: z .optional(z.string()) .register(z.globalRegistry, { description: 'New organization name' }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, name }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.organization.updateOrganization({ name, }); return ok(result); } catch (error) { return fail('Update organization', error); } } ); // ── Projects ────────────────────────────────────────────────────────────── server.registerTool( 'intlayer-cms-projects-list', { title: 'List CMS Projects', description: 'List all Intlayer CMS projects for the selected organization. These are server-side projects, not local project directories.', inputSchema: { ...authSchema, page: z .optional(z.number()) .register(z.globalRegistry, { description: 'Page number (1-based)' }), pageSize: z .optional(z.number()) .register(z.globalRegistry, { description: 'Items per page' }), }, annotations: { readOnlyHint: true }, }, async ({ clientId, clientSecret, page, pageSize }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.project.getProjects({ page, pageSize }); return ok(result); } catch (error) { return fail('List CMS projects', error); } } ); server.registerTool( 'intlayer-cms-project-select', { title: 'Select CMS Project', description: 'Select a CMS project as the current active project. Required before accessing project-specific dictionaries.', inputSchema: { ...authSchema, projectId: z .string() .register(z.globalRegistry, { description: 'Project ID to select' }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, projectId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.project.selectProject(projectId); return ok(result); } catch (error) { return fail('Select CMS project', error); } } ); server.registerTool( 'intlayer-cms-project-create', { title: 'Create CMS Project', description: 'Create a new CMS project in the selected organization.', inputSchema: { ...authSchema, name: z .string() .register(z.globalRegistry, { description: 'Project name' }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, name }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.project.addProject({ name }); return ok(result); } catch (error) { return fail('Create CMS project', error); } } ); server.registerTool( 'intlayer-cms-project-update', { title: 'Update CMS Project', description: 'Update the selected CMS project settings.', inputSchema: { ...authSchema, name: z .optional(z.string()) .register(z.globalRegistry, { description: 'New project name' }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, name }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.project.updateProject({ name }); return ok(result); } catch (error) { return fail('Update CMS project', error); } } ); // ── Environments ────────────────────────────────────────────────────────── server.registerTool( 'intlayer-cms-environment-select', { title: 'Select CMS Environment', description: 'Select a CMS environment as the current active environment. Required before accessing environment-specific dictionaries.', inputSchema: { ...authSchema, environmentId: z.string().register(z.globalRegistry, { description: 'Environment ID to select', }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, environmentId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.selectEnvironment(environmentId); return ok(result); } catch (error) { return fail('Select CMS environment', error); } } ); server.registerTool( 'intlayer-cms-environment-reset', { title: 'Reset CMS Environment to Production', description: 'Reset the current active environment to the default production environment.', inputSchema: { ...authSchema, }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.resetToProductionEnvironment(); return ok(result); } catch (error) { return fail('Reset CMS environment', error); } } ); server.registerTool( 'intlayer-cms-environment-create', { title: 'Create CMS Environment', description: 'Create a new CMS environment in the selected project.', inputSchema: { ...authSchema, name: z .string() .register(z.globalRegistry, { description: 'Environment name' }), }, annotations: { destructiveHint: false }, }, async ({ clientId, clientSecret, name }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.addEnvironment({ name }); return ok(result); } catch (error) { return fail('Create CMS environment', error); } } ); server.registerTool( 'intlayer-cms-environment-update', { title: 'Update CMS Environment', description: 'Update the selected CMS environment settings.', inputSchema: { ...authSchema, environmentId: z.string().register(z.globalRegistry, { description: 'Environment ID to update', }), name: z .optional(z.string()) .register(z.globalRegistry, { description: 'New environment name' }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, environmentId, name }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.updateEnvironment(environmentId, { name, }); return ok(result); } catch (error) { return fail('Update CMS environment', error); } } ); server.registerTool( 'intlayer-cms-environment-delete', { title: 'Delete CMS Environment', description: 'Delete a CMS environment by its ID.', inputSchema: { ...authSchema, environmentId: z.string().register(z.globalRegistry, { description: 'Environment ID to delete', }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, environmentId }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.deleteEnvironment(environmentId); return ok(result); } catch (error) { return fail('Delete CMS environment', error); } } ); server.registerTool( 'intlayer-cms-environment-migrate', { title: 'Migrate CMS Environment', description: 'Migrate dictionaries and configuration from one environment to another.', inputSchema: { ...authSchema, sourceEnvironmentId: z .string() .register(z.globalRegistry, { description: 'Source environment ID' }), targetEnvironmentId: z .string() .register(z.globalRegistry, { description: 'Target environment ID' }), strategy: z .enum(['overwrite', 'fill-missing']) .register(z.globalRegistry, { description: 'Migration strategy: overwrite existing or only fill missing', }), migrateContent: z.optional(z.boolean()).register(z.globalRegistry, { description: 'Whether to migrate content (dictionaries)', }), migrateConfiguration: z .optional(z.boolean()) .register(z.globalRegistry, { description: 'Whether to migrate environment configuration', }), }, annotations: { destructiveHint: true }, }, async ({ clientId, clientSecret, sourceEnvironmentId, targetEnvironmentId, strategy, migrateContent, migrateConfiguration, }) => { try { const api = await getAPI(clientId, clientSecret); const result = await api.environment.migrateEnvironment({ sourceEnvironmentId, targetEnvironmentId, strategy, migrateContent, migrateConfiguration, }); return ok(result); } catch (error) { return fail('Migrate CMS environment', error); } } ); };