From 66ab4ada6e8c92a01a99f26fc29b42eed4d29c34 Mon Sep 17 00:00:00 2001 From: Jackwood Date: Thu, 9 Apr 2026 21:30:20 -0400 Subject: [PATCH] refactor(mcp): remove Sombra REST client and all REST-backed tools Remove TranscendRestClient and all MCP tools that depend on Sombra encrypted REST endpoints. These tools cannot function without Sombra and will be re-evaluated once the broader Sombra integration strategy is finalized. Removed tools (18 total): - Preferences: all 6 tools (query, upsert, delete, append/update/delete identifiers) - DSR: 8 tools (submit, employee_submit, poll_status, download_keys, list/enrich_identifiers, respond_access, respond_erasure) - Discovery: 2 tools (classify_text, ner_extract) - Consent: 2 tools (get_preferences, set_preferences) Remaining tools: 52 (all GraphQL-only) --- packages/mcp/README.md | 42 +- packages/mcp/mcp-server-admin/README.md | 9 +- packages/mcp/mcp-server-admin/src/cli.ts | 5 +- .../src/tools/admin_test_connection.ts | 20 +- .../mcp/mcp-server-admin/tests/admin.test.ts | 10 - packages/mcp/mcp-server-assessments/README.md | 9 +- .../mcp/mcp-server-assessments/src/cli.ts | 5 +- packages/mcp/mcp-server-consent/README.md | 9 +- packages/mcp/mcp-server-consent/src/index.ts | 7 - .../src/tools/consent_get_preferences.ts | 32 -- .../src/tools/consent_set_preferences.ts | 43 -- .../mcp/mcp-server-consent/src/tools/index.ts | 4 - .../mcp-server-consent/tests/consent.test.ts | 3 - packages/mcp/mcp-server-core/README.md | 4 +- .../src/clients/rest-client.ts | 366 ------------------ packages/mcp/mcp-server-core/src/index.ts | 1 - .../src/server/create-server.ts | 10 +- .../mcp/mcp-server-core/src/tools/types.ts | 3 - packages/mcp/mcp-server-discovery/README.md | 9 +- packages/mcp/mcp-server-discovery/src/cli.ts | 5 +- .../mcp/mcp-server-discovery/src/index.ts | 2 - .../src/tools/discovery_classify_text.ts | 37 -- .../src/tools/discovery_ner_extract.ts | 35 -- .../mcp-server-discovery/src/tools/index.ts | 4 - .../tests/discovery.test.ts | 16 +- packages/mcp/mcp-server-dsr/README.md | 9 +- packages/mcp/mcp-server-dsr/src/cli.ts | 5 +- packages/mcp/mcp-server-dsr/src/graphql.ts | 31 -- packages/mcp/mcp-server-dsr/src/index.ts | 14 - .../src/tools/dsr_download_keys.ts | 27 -- .../src/tools/dsr_employee_submit.ts | 46 --- .../src/tools/dsr_enrich_identifiers.ts | 34 -- .../src/tools/dsr_list_identifiers.ts | 31 -- .../src/tools/dsr_poll_status.ts | 23 -- .../src/tools/dsr_respond_access.ts | 36 -- .../src/tools/dsr_respond_erasure.ts | 36 -- .../mcp-server-dsr/src/tools/dsr_submit.ts | 50 --- .../mcp/mcp-server-dsr/src/tools/index.ts | 16 - packages/mcp/mcp-server-dsr/tests/dsr.test.ts | 41 +- packages/mcp/mcp-server-inventory/README.md | 9 +- packages/mcp/mcp-server-inventory/src/cli.ts | 5 +- packages/mcp/mcp-server-preferences/README.md | 9 +- .../mcp/mcp-server-preferences/src/cli.ts | 9 +- .../mcp/mcp-server-preferences/src/index.ts | 33 -- .../mcp-server-preferences/src/tools/index.ts | 17 +- .../tools/preferences_append_identifiers.ts | 39 -- .../src/tools/preferences_delete.ts | 36 -- .../tools/preferences_delete_identifiers.ts | 39 -- .../src/tools/preferences_query.ts | 38 -- .../tools/preferences_update_identifiers.ts | 47 --- .../src/tools/preferences_upsert.ts | 54 --- .../tests/preferences.test.ts | 91 +---- packages/mcp/mcp-server-workflows/README.md | 9 +- packages/mcp/mcp-server-workflows/src/cli.ts | 5 +- packages/mcp/mcp-server/README.md | 19 +- packages/mcp/mcp-server/src/cli.ts | 8 +- packages/mcp/mcp-server/src/graphql-client.ts | 3 - packages/mcp/mcp-server/src/registry.ts | 8 +- .../mcp/mcp-server/tests/annotations.test.ts | 7 - .../mcp/mcp-server/tests/registry.test.ts | 15 +- .../mcp-server/tests/umbrella-tool-count.ts | 2 +- 61 files changed, 106 insertions(+), 1485 deletions(-) delete mode 100644 packages/mcp/mcp-server-consent/src/tools/consent_get_preferences.ts delete mode 100644 packages/mcp/mcp-server-consent/src/tools/consent_set_preferences.ts delete mode 100644 packages/mcp/mcp-server-core/src/clients/rest-client.ts delete mode 100644 packages/mcp/mcp-server-discovery/src/tools/discovery_classify_text.ts delete mode 100644 packages/mcp/mcp-server-discovery/src/tools/discovery_ner_extract.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_download_keys.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_employee_submit.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_enrich_identifiers.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_list_identifiers.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_poll_status.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_respond_access.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_respond_erasure.ts delete mode 100644 packages/mcp/mcp-server-dsr/src/tools/dsr_submit.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_append_identifiers.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_delete.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_delete_identifiers.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_query.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_update_identifiers.ts delete mode 100644 packages/mcp/mcp-server-preferences/src/tools/preferences_upsert.ts diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 5dbbe7a8..68dee0a1 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -6,10 +6,10 @@ ## Prerequisites -- **Node.js** ≥ 22.12 (see each CLI package’s `engines` in `package.json`). +- **Node.js** ≥ 22.12 (see each CLI package's `engines` in `package.json`). - Packages are **alpha** and **not yet published to npm**. Global install and `npx` examples below assume a future registry release. **Until then**, clone this repository: copy [`secret.env.example`](../../secret.env.example) to **`secret.env`** at the repo root and set `TRANSCEND_API_KEY`; then from the repo root run `pnpm exec turbo run build --filter="@transcend-io/..."` (trailing `...` includes dependencies such as `mcp-server-core`), then `set -a && source ./secret.env && set +a` and `pnpm -F @transcend-io/ exec node ./dist/cli.mjs` (or use [`scripts/mcp-run.sh`](../../scripts/mcp-run.sh) — see **Run from the monorepo** in each package README and [CONTRIBUTING.md](../../CONTRIBUTING.md#mcp-servers)). -In client config, `npx` with `-y @transcend-io/...` runs that package’s published `bin` (see `package.json` in each package). +In client config, `npx` with `-y @transcend-io/...` runs that package's published `bin` (see `package.json` in each package). ## Choosing a server @@ -17,7 +17,7 @@ There are two ways to consume the MCP tools, and they can be mixed freely. ### Unified server -Install **`@transcend-io/mcp-server`** to get every tool (71 across all domains) in a single process. This is the fastest way to get started and is ideal when your agent can handle a large tool set. +Install **`@transcend-io/mcp-server`** to get every tool across all domains in a single process. This is the fastest way to get started and is ideal when your agent can handle a large tool set. ```json { @@ -63,18 +63,18 @@ Install only the domains you need. Smaller tool counts help AI agents stay focus ## Packages -| Package | Binary | Tools | Description | -| ----------------------------------------------------- | --------------------------- | ----: | ------------------------------------------------ | -| [`mcp-server`](./mcp-server/) | `transcend-mcp` | 71 | Unified server — all tools in one process | -| [`mcp-server-admin`](./mcp-server-admin/) | `transcend-mcp-admin` | 8 | Organization, users, teams, API keys | -| [`mcp-server-assessments`](./mcp-server-assessments/) | `transcend-mcp-assessments` | 14 | Privacy assessments, templates, groups | -| [`mcp-server-consent`](./mcp-server-consent/) | `transcend-mcp-consent` | 12 | Consent management, cookie & data-flow triage | -| [`mcp-server-core`](./mcp-server-core/) | — | — | Shared infrastructure (not installed directly) | -| [`mcp-server-discovery`](./mcp-server-discovery/) | `transcend-mcp-discovery` | 6 | Data discovery, classification, NER | -| [`mcp-server-dsr`](./mcp-server-dsr/) | `transcend-mcp-dsr` | 12 | Data subject requests (submit, track, respond) | -| [`mcp-server-inventory`](./mcp-server-inventory/) | `transcend-mcp-inventory` | 10 | Data inventory, silos, vendors, data points | -| [`mcp-server-preferences`](./mcp-server-preferences/) | `transcend-mcp-preferences` | 6 | Privacy preference store (query, upsert, delete) | -| [`mcp-server-workflows`](./mcp-server-workflows/) | `transcend-mcp-workflows` | 3 | Workflow & email-template configuration | +| Package | Binary | Tools | Description | +| ----------------------------------------------------- | --------------------------- | ----: | ---------------------------------------------- | +| [`mcp-server`](./mcp-server/) | `transcend-mcp` | 52 | Unified server — all tools in one process | +| [`mcp-server-admin`](./mcp-server-admin/) | `transcend-mcp-admin` | 8 | Organization, users, teams, API keys | +| [`mcp-server-assessments`](./mcp-server-assessments/) | `transcend-mcp-assessments` | 14 | Privacy assessments, templates, groups | +| [`mcp-server-consent`](./mcp-server-consent/) | `transcend-mcp-consent` | 9 | Consent management, cookie & data-flow triage | +| [`mcp-server-core`](./mcp-server-core/) | — | — | Shared infrastructure (not installed directly) | +| [`mcp-server-discovery`](./mcp-server-discovery/) | `transcend-mcp-discovery` | 4 | Data discovery, classification scans | +| [`mcp-server-dsr`](./mcp-server-dsr/) | `transcend-mcp-dsr` | 4 | Data subject requests (list, details, cancel) | +| [`mcp-server-inventory`](./mcp-server-inventory/) | `transcend-mcp-inventory` | 10 | Data inventory, silos, vendors, data points | +| [`mcp-server-preferences`](./mcp-server-preferences/) | `transcend-mcp-preferences` | 0 | Privacy preference store (temporarily empty) | +| [`mcp-server-workflows`](./mcp-server-workflows/) | `transcend-mcp-workflows` | 3 | Workflow & email-template configuration | See each package's README for full tool lists, detailed environment variable docs, and client configuration examples. @@ -92,14 +92,13 @@ See each package's README for full tool lists, detailed environment variable doc │ └─────────────┼────────────┘ │ │ ▼ │ │ mcp-server-core │ -│ (GraphQL base, REST client, validation) │ +│ (GraphQL base, validation) │ └──────────────────────────────────────────────────────────┘ ``` Each domain package (admin, consent, dsr, ...) is a self-contained MCP server with its own CLI entry point. It can run standalone or be composed into the unified server. All domain packages depend on `mcp-server-core` for shared infrastructure: - **`TranscendGraphQLBase`** — base class extended by each domain's GraphQL mixin -- **`TranscendRestClient`** — REST client for the Sombra API (used by DSR, preferences, and discovery) - **`createMCPServer`** — factory that bootstraps a stdio MCP server from tool definitions - **Validation & helpers** — Zod schemas, `validateArgs`, `createToolResult`, `createListResult` @@ -109,11 +108,10 @@ The unified `mcp-server` package aggregates tools via `ToolRegistry` and compose All servers share the same environment variables: -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ------------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra REST API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** store these in root **`secret.env`** (from [`secret.env.example`](../../secret.env.example)); load with `source` or [`scripts/mcp-run.sh`](../../scripts/mcp-run.sh). See [CONTRIBUTING.md](../../CONTRIBUTING.md#mcp-servers). diff --git a/packages/mcp/mcp-server-admin/README.md b/packages/mcp/mcp-server-admin/README.md index d648cf01..33492591 100644 --- a/packages/mcp/mcp-server-admin/README.md +++ b/packages/mcp/mcp-server-admin/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-admin/src/cli.ts b/packages/mcp/mcp-server-admin/src/cli.ts index 2a162fbe..447c5839 100644 --- a/packages/mcp/mcp-server-admin/src/cli.ts +++ b/packages/mcp/mcp-server-admin/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { AdminMixin } from './graphql.js'; import { getAdminTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-admin', version: '1.0.0', getTools: getAdminTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new AdminMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-admin/src/tools/admin_test_connection.ts b/packages/mcp/mcp-server-admin/src/tools/admin_test_connection.ts index 615708dc..49301f34 100644 --- a/packages/mcp/mcp-server-admin/src/tools/admin_test_connection.ts +++ b/packages/mcp/mcp-server-admin/src/tools/admin_test_connection.ts @@ -8,30 +8,24 @@ import { import type { AdminMixin } from '../graphql.js'; export function createAdminTestConnectionTool(clients: ToolClients) { - const { rest } = clients; const graphql = clients.graphql as AdminMixin; return defineTool({ name: 'admin_test_connection', - description: 'Test connectivity to both Transcend REST and GraphQL APIs', + description: 'Test connectivity to the Transcend GraphQL API', category: 'Admin', readOnly: true, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, zodSchema: EmptySchema, handler: async (_args) => { - const [graphqlConnected, restConnected] = await Promise.all([ - graphql.testConnection(), - rest.testConnection(), - ]); - const allConnected = graphqlConnected && restConnected; + const connected = await graphql.testConnection(); return createToolResult(true, { - connected: allConnected, + connected, details: { - graphql: { connected: graphqlConnected, url: graphql.getBaseUrl() }, - rest: { connected: restConnected, url: rest.getBaseUrl() }, + graphql: { connected, url: graphql.getBaseUrl() }, }, - message: allConnected - ? 'Successfully connected to all Transcend APIs' - : 'Some API connections failed - check details', + message: connected + ? 'Successfully connected to Transcend GraphQL API' + : 'GraphQL API connection failed - check details', timestamp: new Date().toISOString(), }); }, diff --git a/packages/mcp/mcp-server-admin/tests/admin.test.ts b/packages/mcp/mcp-server-admin/tests/admin.test.ts index f88c7dfb..2346b3fe 100644 --- a/packages/mcp/mcp-server-admin/tests/admin.test.ts +++ b/packages/mcp/mcp-server-admin/tests/admin.test.ts @@ -26,11 +26,6 @@ describe('Admin Tools', () => { getBaseUrl: ReturnType; }; - let mockRest: { - testConnection: ReturnType; - getBaseUrl: ReturnType; - }; - beforeEach(() => { mockGraphql = { getOrganization: vi.fn(), @@ -43,15 +38,10 @@ describe('Admin Tools', () => { testConnection: vi.fn(), getBaseUrl: vi.fn().mockReturnValue('https://api.transcend.io'), }; - mockRest = { - testConnection: vi.fn(), - getBaseUrl: vi.fn().mockReturnValue('https://multi-tenant.sombra.transcend.io'), - }; }); const getTools = () => getAdminTools({ - rest: mockRest as never, graphql: mockGraphql, }); diff --git a/packages/mcp/mcp-server-assessments/README.md b/packages/mcp/mcp-server-assessments/README.md index 063279a3..af4484fe 100644 --- a/packages/mcp/mcp-server-assessments/README.md +++ b/packages/mcp/mcp-server-assessments/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-assessments/src/cli.ts b/packages/mcp/mcp-server-assessments/src/cli.ts index 66f69048..de841db3 100644 --- a/packages/mcp/mcp-server-assessments/src/cli.ts +++ b/packages/mcp/mcp-server-assessments/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { AssessmentsMixin } from './graphql.js'; import { getAssessmentTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-assessments', version: '1.0.0', getTools: getAssessmentTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new AssessmentsMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-consent/README.md b/packages/mcp/mcp-server-consent/README.md index f6cdd7b8..19d3004f 100644 --- a/packages/mcp/mcp-server-consent/README.md +++ b/packages/mcp/mcp-server-consent/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-consent/src/index.ts b/packages/mcp/mcp-server-consent/src/index.ts index bfbc85e8..d5f58eed 100644 --- a/packages/mcp/mcp-server-consent/src/index.ts +++ b/packages/mcp/mcp-server-consent/src/index.ts @@ -1,13 +1,6 @@ export { getConsentTools } from './tools/index.js'; export { resolveAirgapBundleId } from './resolveAirgapBundleId.js'; -export { GetPreferencesSchema, type GetPreferencesInput } from './tools/consent_get_preferences.js'; -export { - PurposeConsentSchema, - type PurposeConsentInput, - SetPreferencesSchema, - type SetPreferencesInput, -} from './tools/consent_set_preferences.js'; export { ListAirgapBundlesSchema, type ListAirgapBundlesInput, diff --git a/packages/mcp/mcp-server-consent/src/tools/consent_get_preferences.ts b/packages/mcp/mcp-server-consent/src/tools/consent_get_preferences.ts deleted file mode 100644 index eb55fad3..00000000 --- a/packages/mcp/mcp-server-consent/src/tools/consent_get_preferences.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const GetPreferencesSchema = z.object({ - identifier: z.string().describe('User identifier (e.g., email, user ID)'), - partition: z.string().optional().describe('Partition/organization context (optional)'), -}); -export type GetPreferencesInput = z.infer; - -export function createConsentGetPreferencesTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'consent_get_preferences', - description: 'Get consent preferences for a specific user/identifier', - category: 'Consent Management', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: GetPreferencesSchema, - handler: async ({ identifier, partition }) => { - const result = await rest.getConsentPreferences(identifier, partition); - if (!result) { - return createToolResult(true, { - found: false, - message: 'No consent preferences found for this identifier', - }); - } - return createToolResult(true, { - found: true, - preferences: result, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-consent/src/tools/consent_set_preferences.ts b/packages/mcp/mcp-server-consent/src/tools/consent_set_preferences.ts deleted file mode 100644 index 57b960d6..00000000 --- a/packages/mcp/mcp-server-consent/src/tools/consent_set_preferences.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const PurposeConsentSchema = z.object({ - purpose: z.string().describe('Purpose slug'), - enabled: z.boolean().describe('Whether consent is granted'), -}); -export type PurposeConsentInput = z.infer; - -export const SetPreferencesSchema = z.object({ - identifier: z.string().optional().describe('User identifier'), - partition: z.string().describe('Partition/organization context'), - purposes: z.array(PurposeConsentSchema).describe('Array of purpose consent settings'), - confirmed: z.boolean().optional().describe('Whether consent was explicitly confirmed'), -}); -export type SetPreferencesInput = z.infer; - -export function createConsentSetPreferencesTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'consent_set_preferences', - description: 'Set consent preferences for a user (client-side sync)', - category: 'Consent Management', - readOnly: false, - confirmationHint: 'Updates consent preferences for the user', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }, - zodSchema: SetPreferencesSchema, - handler: async ({ partition, identifier, purposes, confirmed }) => { - const result = await rest.syncConsent({ - partition, - identifier, - purposes: purposes.map((p) => ({ - purpose: p.purpose, - enabled: p.enabled, - })), - confirmed, - }); - return createToolResult(true, { - ...result, - message: 'Consent preferences synced successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-consent/src/tools/index.ts b/packages/mcp/mcp-server-consent/src/tools/index.ts index a8bcba72..bc6e9957 100644 --- a/packages/mcp/mcp-server-consent/src/tools/index.ts +++ b/packages/mcp/mcp-server-consent/src/tools/index.ts @@ -1,21 +1,17 @@ import type { ToolDefinition, ToolClients } from '@transcend-io/mcp-server-core'; import { createConsentBulkTriageTool } from './consent_bulk_triage.js'; -import { createConsentGetPreferencesTool } from './consent_get_preferences.js'; import { createConsentGetTriageStatsTool } from './consent_get_triage_stats.js'; import { createConsentListAirgapBundlesTool } from './consent_list_airgap_bundles.js'; import { createConsentListCookiesTool } from './consent_list_cookies.js'; import { createConsentListDataFlowsTool } from './consent_list_data_flows.js'; import { createConsentListPurposesTool } from './consent_list_purposes.js'; import { createConsentListRegimesTool } from './consent_list_regimes.js'; -import { createConsentSetPreferencesTool } from './consent_set_preferences.js'; import { createConsentUpdateCookiesTool } from './consent_update_cookies.js'; import { createConsentUpdateDataFlowsTool } from './consent_update_data_flows.js'; export function getConsentTools(clients: ToolClients): ToolDefinition[] { return [ - createConsentGetPreferencesTool(clients), - createConsentSetPreferencesTool(clients), createConsentListPurposesTool(clients), createConsentListDataFlowsTool(clients), createConsentListCookiesTool(clients), diff --git a/packages/mcp/mcp-server-consent/tests/consent.test.ts b/packages/mcp/mcp-server-consent/tests/consent.test.ts index d930b606..72854ad3 100644 --- a/packages/mcp/mcp-server-consent/tests/consent.test.ts +++ b/packages/mcp/mcp-server-consent/tests/consent.test.ts @@ -3,8 +3,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getConsentTools } from '../src/tools/index.js'; const EXPECTED_TOOL_NAMES = [ - 'consent_get_preferences', - 'consent_set_preferences', 'consent_list_purposes', 'consent_list_data_flows', 'consent_list_cookies', @@ -33,7 +31,6 @@ describe('Consent Tools', () => { const getTools = () => getConsentTools({ - rest: {} as never, graphql: mockGraphql as never, }); diff --git a/packages/mcp/mcp-server-core/README.md b/packages/mcp/mcp-server-core/README.md index d9316880..94ec3cee 100644 --- a/packages/mcp/mcp-server-core/README.md +++ b/packages/mcp/mcp-server-core/README.md @@ -2,7 +2,7 @@ > **Alpha** — this package is under active development and has not yet been published to npm. APIs may change without notice. -Shared infrastructure for all Transcend MCP Server packages. Provides the base GraphQL and REST clients, tool type definitions, validation helpers, error handling, and the `createMCPServer` factory used by each domain server. +Shared infrastructure for all Transcend MCP Server packages. Provides the base GraphQL client, tool type definitions, validation helpers, error handling, and the `createMCPServer` factory used by each domain server. Requires **Node.js ≥ 22.12** (see `engines` in `package.json`) when building or importing this library. @@ -17,7 +17,6 @@ API keys for running a domain or unified MCP server locally belong in the reposi ## What's inside - **`TranscendGraphQLBase`** — Base GraphQL client with query execution, pagination, and logging. Domain packages extend this via mixin classes. -- **`TranscendRestClient`** — REST client for the Sombra API. - **`createMCPServer`** — Bootstraps a stdio MCP server from a list of tool definitions and client factories. - **Tool helpers** — `validateArgs`, `createToolResult`, `createListResult`, common Zod schemas (`PaginationSchema`), and shared TypeScript types (`ToolDefinition`, `ToolClients`, `ToolAnnotations`). - **Error utilities** — `ToolError`, `ErrorCode`, `classifyHttpError` for consistent error responses. @@ -30,7 +29,6 @@ This package is not intended to be installed directly by end users. It is a `wor import { createMCPServer, TranscendGraphQLBase, - TranscendRestClient, validateArgs, createToolResult, z, diff --git a/packages/mcp/mcp-server-core/src/clients/rest-client.ts b/packages/mcp/mcp-server-core/src/clients/rest-client.ts deleted file mode 100644 index a4f2415a..00000000 --- a/packages/mcp/mcp-server-core/src/clients/rest-client.ts +++ /dev/null @@ -1,366 +0,0 @@ -import type { - DSRSubmission, - DSRResponse, - DownloadKey, - EnrichIdentifiersInput, - AccessResponseInput, - ErasureResponseInput, - PreferenceQueryInput, - PreferenceUpsertInput, - UserPreferences, - LLMClassificationInput, - LLMClassificationResult, - NERExtractionInput, - NERExtractionResult, - RequestOptions, -} from '../types/transcend.js'; -import { SimpleLogger, type Logger } from './graphql/base.js'; - -export class TranscendRestClient { - private apiKey: string; - private baseUrl: string; - private logger: Logger; - private defaultTimeout: number; - private defaultRetries: number; - private lastRequestTime: number = 0; - private minRequestInterval: number = 200; - - constructor( - apiKey: string, - baseUrl: string = 'https://multi-tenant.sombra.transcend.io', - logger?: Logger, - ) { - this.apiKey = apiKey; - this.baseUrl = baseUrl.replace(/\/$/, ''); - this.logger = logger || new SimpleLogger(); - this.defaultTimeout = 30000; - this.defaultRetries = 3; - } - - private async rateLimitWait(): Promise { - const now = Date.now(); - const elapsed = now - this.lastRequestTime; - if (elapsed < this.minRequestInterval) { - await new Promise((resolve) => setTimeout(resolve, this.minRequestInterval - elapsed)); - } - this.lastRequestTime = Date.now(); - } - - private async makeRequest( - endpoint: string, - options: RequestInit & RequestOptions = {}, - ): Promise { - await this.rateLimitWait(); - - const url = `${this.baseUrl}${endpoint}`; - const { - timeout = this.defaultTimeout, - retries = this.defaultRetries, - ...fetchOptions - } = options; - - const headers: Record = { - Authorization: `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-Transcend-Version': '2021-11-15', - ...((options.headers as Record) || {}), - }; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - let lastError: Error | null = null; - - for (let attempt = 0; attempt <= retries; attempt++) { - try { - this.logger.debug(`REST request: ${fetchOptions.method || 'GET'} ${url}`, { attempt }); - - const response = await fetch(url, { - ...fetchOptions, - headers, - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const errorText = await response.text(); - const error = new Error( - `REST API error: ${response.status} ${response.statusText} - ${errorText}`, - ); - - if (response.status >= 400 && response.status < 500 && response.status !== 429) { - throw error; - } - - lastError = error; - - if (attempt < retries) { - const delay = Math.pow(2, attempt) * 1000; - this.logger.warn(`Retrying in ${delay}ms...`, { attempt, status: response.status }); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - - throw error; - } - - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - return {} as T; - } - - const text = await response.text(); - if (!text) { - return {} as T; - } - - return JSON.parse(text) as T; - } catch (error) { - clearTimeout(timeoutId); - - if (error instanceof Error && error.name === 'AbortError') { - throw new Error(`Request timeout after ${timeout}ms`); - } - - lastError = error instanceof Error ? error : new Error(String(error)); - - if (attempt < retries) { - const delay = Math.pow(2, attempt) * 1000; - this.logger.warn(`Retrying in ${delay}ms after error...`, { - attempt, - error: lastError.message, - }); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - } - } - - throw lastError || new Error('Request failed after all retries'); - } - - async submitDSR(submission: DSRSubmission): Promise { - const coreIdentifier = submission.coreIdentifier || submission.email; - const payload = { - type: submission.type, - subject: { - email: submission.email, - coreIdentifier, - ...(submission.name && { name: submission.name }), - ...(submission.phone && { phone: submission.phone }), - }, - ...(submission.subjectType && { subjectType: submission.subjectType }), - ...(submission.locale && { locale: submission.locale }), - ...(submission.isSilent !== undefined && { isSilent: submission.isSilent }), - ...(submission.skipSecondaryLookup !== undefined && { - skipSecondaryLookup: submission.skipSecondaryLookup, - }), - ...(submission.additionalIdentifiers && { - additionalIdentifiers: submission.additionalIdentifiers, - }), - }; - return this.makeRequest('/v1/data-subject-request', { - method: 'POST', - body: JSON.stringify(payload), - }); - } - - async getDSRStatus(requestId: string): Promise { - return this.makeRequest(`/v1/data-subject-request/${requestId}`); - } - - async getDSRDownloadKeys(requestId: string): Promise { - const response = await this.makeRequest<{ downloadKeys: DownloadKey[] }>( - `/v1/data-subject-request/${requestId}/download-keys`, - ); - return response.downloadKeys || []; - } - - async downloadDSRFiles(downloadKey: string): Promise { - const url = `${this.baseUrl}/v1/files?key=${encodeURIComponent(downloadKey)}`; - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${this.apiKey}`, - Accept: 'application/octet-stream', - }, - }); - if (!response.ok) { - throw new Error(`Failed to download file: ${response.status} ${response.statusText}`); - } - return response.arrayBuffer(); - } - - async listRequestIdentifiers(requestId: string): Promise[]> { - const response = await this.makeRequest<{ identifiers: Record[] }>( - `/v1/request-identifiers?requestId=${encodeURIComponent(requestId)}`, - ); - return response.identifiers || []; - } - - async enrichIdentifiers(input: EnrichIdentifiersInput): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>('/v1/enrich-identifiers', { - method: 'POST', - body: JSON.stringify(input), - }); - } - - async respondToAccess(input: AccessResponseInput): Promise<{ success: boolean }> { - const payload = { - requestId: input.requestId, - dataSiloId: input.dataSiloId, - ...(input.profiles && { profiles: input.profiles }), - ...(input.files && { files: input.files }), - }; - return this.makeRequest<{ success: boolean }>('/v1/datapoint', { - method: 'POST', - body: JSON.stringify(payload), - }); - } - - async respondToAccessChunked( - input: AccessResponseInput & { chunkIndex: number; totalChunks: number }, - ): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>('/v1/datapoint-chunked', { - method: 'POST', - body: JSON.stringify(input), - }); - } - - async confirmErasure(input: ErasureResponseInput): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>('/v1/data-silo', { - method: 'POST', - body: JSON.stringify({ - requestId: input.requestId, - dataSiloId: input.dataSiloId, - status: 'COMPLETED', - ...(input.profileIds && { profileIds: input.profileIds }), - }), - }); - } - - async getPendingRequests( - dataSiloId: string, - requestType: 'ACCESS' | 'ERASURE', - ): Promise<{ requests: { id: string; identifiers: Record }[] }> { - return this.makeRequest<{ requests: { id: string; identifiers: Record }[] }>( - `/v1/data-silo/${dataSiloId}/pending-requests/${requestType}`, - ); - } - - async queryPreferences(input: PreferenceQueryInput): Promise { - const response = await this.makeRequest<{ preferences: UserPreferences[] }>( - `/v1/preferences/${encodeURIComponent(input.partition)}/query`, - { method: 'POST', body: JSON.stringify({ identifiers: input.identifiers }) }, - ); - return response.preferences || []; - } - - async upsertPreferences( - input: PreferenceUpsertInput, - ): Promise<{ success: boolean; count: number }> { - return this.makeRequest<{ success: boolean; count: number }>('/v1/preferences', { - method: 'POST', - body: JSON.stringify(input), - }); - } - - async deletePreferences( - partition: string, - identifiers: { value: string; type?: string }[], - ): Promise<{ success: boolean; count: number }> { - return this.makeRequest<{ success: boolean; count: number }>( - `/v1/preferences/${encodeURIComponent(partition)}/delete`, - { method: 'DELETE', body: JSON.stringify({ identifiers }) }, - ); - } - - async appendIdentifiers( - partition: string, - userId: string, - identifiers: { value: string; type?: string }[], - ): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>( - `/v1/preferences/${encodeURIComponent(partition)}/append-identifiers`, - { method: 'POST', body: JSON.stringify({ userId, identifiers }) }, - ); - } - - async updateIdentifiers( - partition: string, - userId: string, - identifiers: { oldValue: string; newValue: string; type?: string }[], - ): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>( - `/v1/preferences/${encodeURIComponent(partition)}/update-identifiers`, - { method: 'PUT', body: JSON.stringify({ userId, identifiers }) }, - ); - } - - async deleteIdentifiers( - partition: string, - userId: string, - identifiers: { value: string; type?: string }[], - ): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>( - `/v1/preferences/${encodeURIComponent(partition)}/delete-identifiers`, - { method: 'DELETE', body: JSON.stringify({ userId, identifiers }) }, - ); - } - - async getConsentPreferences( - identifier: string, - partition?: string, - ): Promise { - const params = new URLSearchParams({ identifier }); - if (partition) params.set('partition', partition); - try { - return await this.makeRequest(`/v1/consent-preferences?${params}`); - } catch { - return null; - } - } - - async syncConsent(preferences: UserPreferences): Promise<{ success: boolean }> { - return this.makeRequest<{ success: boolean }>('/sync', { - method: 'POST', - body: JSON.stringify(preferences), - }); - } - - async classifyText(input: LLMClassificationInput): Promise { - const payload = { inputList: input.texts, labels: input.categories || [] }; - const response = await this.makeRequest<{ results: LLMClassificationResult[] }>( - '/llm/classify-text', - { method: 'POST', body: JSON.stringify(payload) }, - ); - return response.results || []; - } - - async extractEntities(input: NERExtractionInput): Promise { - const payload = { inputList: [input.text], labels: input.entityTypes || [] }; - return this.makeRequest('/classify/unstructured-text', { - method: 'POST', - body: JSON.stringify(payload), - }); - } - - async getSombraPublicKey(): Promise<{ key: string }> { - return this.makeRequest<{ key: string }>('/public-keys/sombra-general-signing-key'); - } - - async testConnection(): Promise { - try { - await this.getSombraPublicKey(); - return true; - } catch (error) { - this.logger.error('REST connection test failed', error); - return false; - } - } - - getBaseUrl(): string { - return this.baseUrl; - } -} diff --git a/packages/mcp/mcp-server-core/src/index.ts b/packages/mcp/mcp-server-core/src/index.ts index 0ce8d67d..55e788c7 100644 --- a/packages/mcp/mcp-server-core/src/index.ts +++ b/packages/mcp/mcp-server-core/src/index.ts @@ -1,6 +1,5 @@ export { TranscendGraphQLBase, SimpleLogger } from './clients/graphql/base.js'; export type { Logger, ListOptions } from './clients/graphql/base.js'; -export { TranscendRestClient } from './clients/rest-client.js'; export { ToolError, ErrorCode, classifyHttpError } from './errors.js'; diff --git a/packages/mcp/mcp-server-core/src/server/create-server.ts b/packages/mcp/mcp-server-core/src/server/create-server.ts index 27302cd9..172845bd 100644 --- a/packages/mcp/mcp-server-core/src/server/create-server.ts +++ b/packages/mcp/mcp-server-core/src/server/create-server.ts @@ -4,7 +4,6 @@ import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-sc import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { SimpleLogger } from '../clients/graphql/base.js'; -import { TranscendRestClient } from '../clients/rest-client.js'; import { createErrorResult, createToolResult } from '../tools/helpers.js'; import type { ToolDefinition, ToolClients } from '../tools/types.js'; @@ -16,14 +15,13 @@ export interface MCPServerOptions { /** Factory that returns tool definitions given API clients */ getTools: (clients: ToolClients) => ToolDefinition[]; /** Optional custom client factory */ - createClients?: (apiKey: string, sombraUrl: string, graphqlUrl: string) => ToolClients; + createClients?: (apiKey: string, graphqlUrl: string) => ToolClients; } export async function createMCPServer(options: MCPServerOptions): Promise { const logger = new SimpleLogger(); const apiKey = process.env.TRANSCEND_API_KEY; - const sombraUrl = process.env.TRANSCEND_API_URL || 'https://multi-tenant.sombra.transcend.io'; const graphqlUrl = process.env.TRANSCEND_GRAPHQL_URL || 'https://api.transcend.io'; if (!apiKey) { @@ -31,12 +29,11 @@ export async function createMCPServer(options: MCPServerOptions): Promise process.exit(1); } - logger.info('Initializing Transcend API clients...', { sombraUrl, graphqlUrl }); + logger.info('Initializing Transcend API clients...', { graphqlUrl }); const clients = options.createClients - ? options.createClients(apiKey, sombraUrl, graphqlUrl) + ? options.createClients(apiKey, graphqlUrl) : { - rest: new TranscendRestClient(apiKey, sombraUrl), graphql: new (await import('../clients/graphql/base.js')).TranscendGraphQLBase( apiKey, graphqlUrl, @@ -124,7 +121,6 @@ export async function createMCPServer(options: MCPServerOptions): Promise await server.connect(transport); logger.info(`${options.name} started successfully`, { - sombraUrl, graphqlUrl, tools: toolMap.size, }); diff --git a/packages/mcp/mcp-server-core/src/tools/types.ts b/packages/mcp/mcp-server-core/src/tools/types.ts index 5f44b043..34df02d8 100644 --- a/packages/mcp/mcp-server-core/src/tools/types.ts +++ b/packages/mcp/mcp-server-core/src/tools/types.ts @@ -1,7 +1,6 @@ import { type z } from 'zod'; import type { TranscendGraphQLBase } from '../clients/graphql/base.js'; -import type { TranscendRestClient } from '../clients/rest-client.js'; export interface ToolAnnotations { /** Whether this tool only reads data */ @@ -33,8 +32,6 @@ export interface ToolDefinition { } export interface ToolClients { - /** REST API client */ - rest: TranscendRestClient; /** GraphQL API client */ graphql: TranscendGraphQLBase; } diff --git a/packages/mcp/mcp-server-discovery/README.md b/packages/mcp/mcp-server-discovery/README.md index 1858e6a9..d34b2732 100644 --- a/packages/mcp/mcp-server-discovery/README.md +++ b/packages/mcp/mcp-server-discovery/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-discovery/src/cli.ts b/packages/mcp/mcp-server-discovery/src/cli.ts index 80a73263..b175ea65 100644 --- a/packages/mcp/mcp-server-discovery/src/cli.ts +++ b/packages/mcp/mcp-server-discovery/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { DiscoveryMixin } from './graphql.js'; import { getDiscoveryTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-discovery', version: '1.0.0', getTools: getDiscoveryTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new DiscoveryMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-discovery/src/index.ts b/packages/mcp/mcp-server-discovery/src/index.ts index 5e6dfe52..6c734c06 100644 --- a/packages/mcp/mcp-server-discovery/src/index.ts +++ b/packages/mcp/mcp-server-discovery/src/index.ts @@ -1,9 +1,7 @@ export { getDiscoveryTools } from './tools/index.js'; export { DiscoveryMixin } from './graphql.js'; -export { ClassifyTextSchema, type ClassifyTextInput } from './tools/discovery_classify_text.js'; export { GetScanSchema, type GetScanInput } from './tools/discovery_get_scan.js'; export { ListPluginsSchema, type ListPluginsInput } from './tools/discovery_list_plugins.js'; export { ListScansSchema, type ListScansInput } from './tools/discovery_list_scans.js'; -export { NerExtractSchema, type NerExtractInput } from './tools/discovery_ner_extract.js'; export { StartScanSchema, type StartScanInput } from './tools/discovery_start_scan.js'; diff --git a/packages/mcp/mcp-server-discovery/src/tools/discovery_classify_text.ts b/packages/mcp/mcp-server-discovery/src/tools/discovery_classify_text.ts deleted file mode 100644 index a1fa5988..00000000 --- a/packages/mcp/mcp-server-discovery/src/tools/discovery_classify_text.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const ClassifyTextSchema = z.object({ - texts: z.array(z.string()).describe('Array of text strings to classify'), - categories: z - .array(z.string()) - .optional() - .describe('Specific categories to classify against (optional)'), - model: z.string().optional().describe('LLM model to use for classification (optional)'), -}); -export type ClassifyTextInput = z.infer; - -export function createDiscoveryClassifyTextTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'discovery_classify_text', - description: - "Classify text content using Transcend's LLM classifier to identify data categories", - category: 'Data Discovery', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: ClassifyTextSchema, - handler: async ({ texts, categories, model }) => { - const results = await rest.classifyText({ - texts, - categories, - model, - }); - - return createToolResult(true, { - results, - inputCount: texts.length, - message: `Classified ${texts.length} text(s) successfully`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-discovery/src/tools/discovery_ner_extract.ts b/packages/mcp/mcp-server-discovery/src/tools/discovery_ner_extract.ts deleted file mode 100644 index 4dade897..00000000 --- a/packages/mcp/mcp-server-discovery/src/tools/discovery_ner_extract.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const NerExtractSchema = z.object({ - text: z.string().describe('Text to extract entities from'), - entity_types: z - .array(z.string()) - .optional() - .describe('Specific entity types to extract (optional)'), -}); -export type NerExtractInput = z.infer; - -export function createDiscoveryNerExtractTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'discovery_ner_extract', - description: 'Extract named entities (PII, organizations, locations, etc.) from text using NER', - category: 'Data Discovery', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: NerExtractSchema, - handler: async ({ text, entity_types }) => { - const result = await rest.extractEntities({ - text, - entityTypes: entity_types, - }); - - return createToolResult(true, { - entities: result.entities, - entityCount: result.entities.length, - entityTypes: [...new Set(result.entities.map((e) => e.type))], - message: `Extracted ${result.entities.length} entities from text`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-discovery/src/tools/index.ts b/packages/mcp/mcp-server-discovery/src/tools/index.ts index 6f4462a6..6fb538f7 100644 --- a/packages/mcp/mcp-server-discovery/src/tools/index.ts +++ b/packages/mcp/mcp-server-discovery/src/tools/index.ts @@ -1,16 +1,12 @@ import type { ToolDefinition, ToolClients } from '@transcend-io/mcp-server-core'; -import { createDiscoveryClassifyTextTool } from './discovery_classify_text.js'; import { createDiscoveryGetScanTool } from './discovery_get_scan.js'; import { createDiscoveryListPluginsTool } from './discovery_list_plugins.js'; import { createDiscoveryListScansTool } from './discovery_list_scans.js'; -import { createDiscoveryNerExtractTool } from './discovery_ner_extract.js'; import { createDiscoveryStartScanTool } from './discovery_start_scan.js'; export function getDiscoveryTools(clients: ToolClients): ToolDefinition[] { return [ - createDiscoveryClassifyTextTool(clients), - createDiscoveryNerExtractTool(clients), createDiscoveryListScansTool(clients), createDiscoveryStartScanTool(clients), createDiscoveryGetScanTool(clients), diff --git a/packages/mcp/mcp-server-discovery/tests/discovery.test.ts b/packages/mcp/mcp-server-discovery/tests/discovery.test.ts index c395a469..e1db9761 100644 --- a/packages/mcp/mcp-server-discovery/tests/discovery.test.ts +++ b/packages/mcp/mcp-server-discovery/tests/discovery.test.ts @@ -3,8 +3,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getDiscoveryTools } from '../src/tools.js'; const EXPECTED_TOOL_NAMES = [ - 'discovery_classify_text', - 'discovery_ner_extract', 'discovery_list_scans', 'discovery_start_scan', 'discovery_get_scan', @@ -19,11 +17,6 @@ describe('Discovery Tools', () => { listDiscoveryPlugins: ReturnType; }; - let mockRest: { - classifyText: ReturnType; - nerExtract: ReturnType; - }; - beforeEach(() => { mockGraphql = { listClassificationScans: vi.fn(), @@ -31,21 +24,16 @@ describe('Discovery Tools', () => { getClassificationScan: vi.fn(), listDiscoveryPlugins: vi.fn(), }; - mockRest = { - classifyText: vi.fn(), - nerExtract: vi.fn(), - }; }); const getTools = () => getDiscoveryTools({ - rest: mockRest as never, graphql: mockGraphql, }); - it('registers exactly 6 tools with expected names', () => { + it('registers exactly 4 tools with expected names', () => { const tools = getTools(); - expect(tools).toHaveLength(6); + expect(tools).toHaveLength(4); expect(tools.map((t) => t.name)).toEqual([...EXPECTED_TOOL_NAMES]); }); diff --git a/packages/mcp/mcp-server-dsr/README.md b/packages/mcp/mcp-server-dsr/README.md index 4f37eb5a..5a71e6f5 100644 --- a/packages/mcp/mcp-server-dsr/README.md +++ b/packages/mcp/mcp-server-dsr/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-dsr/src/cli.ts b/packages/mcp/mcp-server-dsr/src/cli.ts index e4683567..afa5893c 100644 --- a/packages/mcp/mcp-server-dsr/src/cli.ts +++ b/packages/mcp/mcp-server-dsr/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { DSRMixin } from './graphql.js'; import { getDSRTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-dsr', version: '1.0.0', getTools: getDSRTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new DSRMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-dsr/src/graphql.ts b/packages/mcp/mcp-server-dsr/src/graphql.ts index 38bec0d8..cf024632 100644 --- a/packages/mcp/mcp-server-dsr/src/graphql.ts +++ b/packages/mcp/mcp-server-dsr/src/graphql.ts @@ -4,7 +4,6 @@ import { type PaginatedResponse, type Request, type RequestDetails, - type RequestType, } from '@transcend-io/mcp-server-core'; export class DSRMixin extends TranscendGraphQLBase { @@ -54,36 +53,6 @@ export class DSRMixin extends TranscendGraphQLBase { return data.request; } - async employeeMakeDataSubjectRequest(input: { - type: RequestType; - email: string; - coreIdentifier?: string; - locale?: string; - isSilent?: boolean; - subjectType: string; - attributes?: Record; - clientMutationId?: string; - }): Promise<{ request: Request; clientMutationId?: string }> { - const mutation = ` - mutation EmployeeMakeDataSubjectRequest($input: EmployeeRequestInput!) { - employeeMakeDataSubjectRequest(input: $input) { - clientMutationId - request { - id - type - status - createdAt - updatedAt - } - } - } - `; - const data = await this.makeRequest<{ - employeeMakeDataSubjectRequest: { request: Request; clientMutationId?: string }; - }>(mutation, { input }); - return data.employeeMakeDataSubjectRequest; - } - async cancelRequest(input: { requestId: string; template?: string; diff --git a/packages/mcp/mcp-server-dsr/src/index.ts b/packages/mcp/mcp-server-dsr/src/index.ts index 73657056..8c7546e6 100644 --- a/packages/mcp/mcp-server-dsr/src/index.ts +++ b/packages/mcp/mcp-server-dsr/src/index.ts @@ -1,20 +1,6 @@ export { getDSRTools } from './tools/index.js'; export { DSRMixin } from './graphql.js'; -export { submitDsrSchema, type SubmitDsrInput } from './tools/dsr_submit.js'; -export { - employeeSubmitDsrSchema, - type EmployeeSubmitDsrInput, -} from './tools/dsr_employee_submit.js'; export { cancelDsrSchema, type CancelDsrInput } from './tools/dsr_cancel.js'; -export { downloadKeysSchema, type DownloadKeysInput } from './tools/dsr_download_keys.js'; export { getDetailsSchema, type GetDetailsInput } from './tools/dsr_get_details.js'; -export { pollStatusSchema, type PollStatusInput } from './tools/dsr_poll_status.js'; -export { - enrichIdentifiersSchema, - type EnrichIdentifiersInput, -} from './tools/dsr_enrich_identifiers.js'; -export { respondAccessSchema, type RespondAccessInput } from './tools/dsr_respond_access.js'; -export { respondErasureSchema, type RespondErasureInput } from './tools/dsr_respond_erasure.js'; -export { listIdentifiersSchema, type ListIdentifiersInput } from './tools/dsr_list_identifiers.js'; export { analyzeDsrSchema, type AnalyzeDsrInput } from './tools/dsr_analyze.js'; diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_download_keys.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_download_keys.ts deleted file mode 100644 index 54c97815..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_download_keys.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; - -export const downloadKeysSchema = z.object({ - request_id: z.string().describe('ID of the completed DSR'), -}); -export type DownloadKeysInput = z.infer; - -export function createDsrDownloadKeysTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_download_keys', - description: - 'Get download keys for a completed Data Subject Request. These keys can be used to download the DSR results.', - category: 'DSR Automation', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: downloadKeysSchema, - handler: async ({ request_id }) => { - const keys = await rest.getDSRDownloadKeys(request_id); - return createToolResult(true, { - downloadKeys: keys, - count: keys.length, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_employee_submit.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_employee_submit.ts deleted file mode 100644 index 66e8f9fa..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_employee_submit.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; -import { RequestAction } from '@transcend-io/privacy-types'; - -import type { DSRMixin } from '../graphql.js'; - -export const employeeSubmitDsrSchema = z.object({ - type: z.nativeEnum(RequestAction).describe('Type of DSR request'), - email: z.string().describe('Email address of the data subject'), - subjectType: z - .string() - .describe('Type of data subject (e.g., customer, employee). Required by the Transcend API.'), - coreIdentifier: z.string().optional().describe('Core identifier for the data subject (optional)'), - locale: z.string().optional().describe('Locale for communications (e.g., en-US)'), - isSilent: z.boolean().optional().describe('Whether to suppress email notifications'), -}); -export type EmployeeSubmitDsrInput = z.infer; - -export function createDsrEmployeeSubmitTool(clients: ToolClients) { - const graphql = clients.graphql as DSRMixin; - - return defineTool({ - name: 'dsr_employee_submit', - description: - 'Submit a Data Subject Request as an employee on behalf of a data subject. Requires subjectType to be specified.', - category: 'DSR Automation', - readOnly: false, - confirmationHint: 'Creates a new data subject request (employee)', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, - zodSchema: employeeSubmitDsrSchema, - handler: async ({ type, email, subjectType, coreIdentifier, locale, isSilent }) => { - const result = await graphql.employeeMakeDataSubjectRequest({ - type, - email, - subjectType, - coreIdentifier, - locale, - isSilent, - }); - return createToolResult(true, { - request: result.request, - clientMutationId: result.clientMutationId, - message: `Employee DSR of type ${type} submitted successfully`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_enrich_identifiers.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_enrich_identifiers.ts deleted file mode 100644 index f768fa87..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_enrich_identifiers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; - -export const enrichIdentifiersSchema = z.object({ - request_id: z.string().describe('ID of the DSR to enrich'), - identifiers: z - .record(z.string(), z.string()) - .describe('Key-value pairs of identifier names and values to add'), -}); -export type EnrichIdentifiersInput = z.infer; - -export function createDsrEnrichIdentifiersTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_enrich_identifiers', - description: - 'Enrich a Data Subject Request with additional identifiers during preflight processing', - category: 'DSR Automation', - readOnly: false, - confirmationHint: 'Adds identifiers to the DSR during preflight', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, - zodSchema: enrichIdentifiersSchema, - handler: async ({ request_id, identifiers }) => { - const result = await rest.enrichIdentifiers({ - requestId: request_id, - identifiers, - }); - return createToolResult(true, { - ...result, - message: 'Identifiers enriched successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_list_identifiers.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_list_identifiers.ts deleted file mode 100644 index 21510f64..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_list_identifiers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - createListResult, - defineTool, - PaginationSchema, - type ToolClients, - z, -} from '@transcend-io/mcp-server-core'; - -export const listIdentifiersSchema = z - .object({ - request_id: z.string().describe('ID of the DSR'), - }) - .merge(PaginationSchema); -export type ListIdentifiersInput = z.infer; - -export function createDsrListIdentifiersTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_list_identifiers', - description: 'List all identifiers attached to a Data Subject Request', - category: 'DSR Automation', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: listIdentifiersSchema, - handler: async ({ request_id }) => { - const identifiers = await rest.listRequestIdentifiers(request_id); - return createListResult(identifiers); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_poll_status.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_poll_status.ts deleted file mode 100644 index 27a9fe3f..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_poll_status.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; - -export const pollStatusSchema = z.object({ - request_id: z.string().describe('ID of the DSR to check'), -}); -export type PollStatusInput = z.infer; - -export function createDsrPollStatusTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_poll_status', - description: 'Poll the current status of a Data Subject Request', - category: 'DSR Automation', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: pollStatusSchema, - handler: async ({ request_id }) => { - const result = await rest.getDSRStatus(request_id); - return createToolResult(true, result); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_access.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_access.ts deleted file mode 100644 index 7dfaf76c..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_access.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; - -export const respondAccessSchema = z.object({ - request_id: z.string().describe('ID of the DSR'), - data_silo_id: z.string().describe('ID of the data silo responding'), - profiles: z - .array(z.record(z.string(), z.unknown())) - .optional() - .describe('Array of profile data objects to return'), -}); -export type RespondAccessInput = z.infer; - -export function createDsrRespondAccessTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_respond_access', - description: 'Respond to an ACCESS request by uploading user data', - category: 'DSR Automation', - readOnly: false, - confirmationHint: 'Uploads access response data for the DSR', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, - zodSchema: respondAccessSchema, - handler: async ({ request_id, data_silo_id, profiles }) => { - const result = await rest.respondToAccess({ - requestId: request_id, - dataSiloId: data_silo_id, - profiles: profiles as Record[] | undefined, - }); - return createToolResult(true, { - ...result, - message: 'Access response submitted successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_erasure.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_erasure.ts deleted file mode 100644 index 7dbe73d7..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_respond_erasure.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; - -export const respondErasureSchema = z.object({ - request_id: z.string().describe('ID of the DSR'), - data_silo_id: z.string().describe('ID of the data silo that completed erasure'), - profile_ids: z - .array(z.string()) - .optional() - .describe('IDs of profiles that were erased (optional)'), -}); -export type RespondErasureInput = z.infer; - -export function createDsrRespondErasureTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_respond_erasure', - description: 'Confirm that data erasure has been completed for a data silo', - category: 'DSR Automation', - readOnly: false, - confirmationHint: 'Confirms erasure completion for the data silo', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }, - zodSchema: respondErasureSchema, - handler: async ({ request_id, data_silo_id, profile_ids }) => { - const result = await rest.confirmErasure({ - requestId: request_id, - dataSiloId: data_silo_id, - profileIds: profile_ids, - }); - return createToolResult(true, { - ...result, - message: 'Erasure confirmation submitted successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/dsr_submit.ts b/packages/mcp/mcp-server-dsr/src/tools/dsr_submit.ts deleted file mode 100644 index 52c6652a..00000000 --- a/packages/mcp/mcp-server-dsr/src/tools/dsr_submit.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createToolResult, defineTool, type ToolClients, z } from '@transcend-io/mcp-server-core'; -import { RequestAction } from '@transcend-io/privacy-types'; - -export const submitDsrSchema = z.object({ - type: z.nativeEnum(RequestAction).describe('Type of DSR request'), - email: z.string().describe('Email address of the data subject'), - subjectType: z - .string() - .describe( - 'Type of data subject (e.g., customer, employee, prospect). Required by the Transcend API.', - ), - coreIdentifier: z - .string() - .optional() - .describe('Core identifier for the data subject (defaults to email if not provided)'), - name: z.string().optional().describe('Name of the data subject (optional)'), - locale: z.string().optional().describe('Locale for communications (e.g., en-US)'), - isSilent: z.boolean().optional().describe('Whether to suppress email notifications'), -}); -export type SubmitDsrInput = z.infer; - -export function createDsrSubmitTool(clients: ToolClients) { - const { rest } = clients; - - return defineTool({ - name: 'dsr_submit', - description: - 'Submit a new Data Subject Request (DSR). Supports ACCESS, ERASURE, RECTIFICATION, and other request types. The coreIdentifier defaults to the email if not provided.', - category: 'DSR Automation', - readOnly: false, - confirmationHint: 'Creates a new data subject request', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, - zodSchema: submitDsrSchema, - handler: async ({ type, email, subjectType, coreIdentifier, name, locale, isSilent }) => { - const result = await rest.submitDSR({ - type, - email, - subjectType, - coreIdentifier, - name, - locale, - isSilent, - }); - return createToolResult(true, { - request: result, - message: `DSR of type ${type} submitted successfully`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-dsr/src/tools/index.ts b/packages/mcp/mcp-server-dsr/src/tools/index.ts index 9f2f424e..d7494ad7 100644 --- a/packages/mcp/mcp-server-dsr/src/tools/index.ts +++ b/packages/mcp/mcp-server-dsr/src/tools/index.ts @@ -2,30 +2,14 @@ import type { ToolDefinition, ToolClients } from '@transcend-io/mcp-server-core' import { createDsrAnalyzeTool } from './dsr_analyze.js'; import { createDsrCancelTool } from './dsr_cancel.js'; -import { createDsrDownloadKeysTool } from './dsr_download_keys.js'; -import { createDsrEmployeeSubmitTool } from './dsr_employee_submit.js'; -import { createDsrEnrichIdentifiersTool } from './dsr_enrich_identifiers.js'; import { createDsrGetDetailsTool } from './dsr_get_details.js'; import { createDsrListTool } from './dsr_list.js'; -import { createDsrListIdentifiersTool } from './dsr_list_identifiers.js'; -import { createDsrPollStatusTool } from './dsr_poll_status.js'; -import { createDsrRespondAccessTool } from './dsr_respond_access.js'; -import { createDsrRespondErasureTool } from './dsr_respond_erasure.js'; -import { createDsrSubmitTool } from './dsr_submit.js'; export function getDSRTools(clients: ToolClients): ToolDefinition[] { return [ - createDsrSubmitTool(clients), - createDsrPollStatusTool(clients), createDsrListTool(clients), createDsrGetDetailsTool(clients), - createDsrDownloadKeysTool(clients), - createDsrListIdentifiersTool(clients), - createDsrEnrichIdentifiersTool(clients), - createDsrRespondAccessTool(clients), - createDsrRespondErasureTool(clients), createDsrCancelTool(clients), - createDsrEmployeeSubmitTool(clients), createDsrAnalyzeTool(clients), ]; } diff --git a/packages/mcp/mcp-server-dsr/tests/dsr.test.ts b/packages/mcp/mcp-server-dsr/tests/dsr.test.ts index d85e9619..bc1d4ffd 100644 --- a/packages/mcp/mcp-server-dsr/tests/dsr.test.ts +++ b/packages/mcp/mcp-server-dsr/tests/dsr.test.ts @@ -2,66 +2,31 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getDSRTools } from '../src/tools.js'; -const EXPECTED_TOOL_NAMES = [ - 'dsr_submit', - 'dsr_poll_status', - 'dsr_list', - 'dsr_get_details', - 'dsr_download_keys', - 'dsr_list_identifiers', - 'dsr_enrich_identifiers', - 'dsr_respond_access', - 'dsr_respond_erasure', - 'dsr_cancel', - 'dsr_employee_submit', - 'dsr_analyze', -] as const; +const EXPECTED_TOOL_NAMES = ['dsr_list', 'dsr_get_details', 'dsr_cancel', 'dsr_analyze'] as const; describe('DSR Tools', () => { let mockGraphql: { listRequests: ReturnType; getRequest: ReturnType; - employeeMakeDataSubjectRequest: ReturnType; cancelRequest: ReturnType; }; - let mockRest: { - submitDSR: ReturnType; - pollDSRStatus: ReturnType; - downloadKeys: ReturnType; - listDSRIdentifiers: ReturnType; - enrichIdentifiers: ReturnType; - respondAccess: ReturnType; - respondErasure: ReturnType; - }; - beforeEach(() => { mockGraphql = { listRequests: vi.fn(), getRequest: vi.fn(), - employeeMakeDataSubjectRequest: vi.fn(), cancelRequest: vi.fn(), }; - mockRest = { - submitDSR: vi.fn(), - pollDSRStatus: vi.fn(), - downloadKeys: vi.fn(), - listDSRIdentifiers: vi.fn(), - enrichIdentifiers: vi.fn(), - respondAccess: vi.fn(), - respondErasure: vi.fn(), - }; }); const getTools = () => getDSRTools({ - rest: mockRest as never, graphql: mockGraphql, }); - it('registers exactly 12 tools with expected names', () => { + it('registers exactly 4 tools with expected names', () => { const tools = getTools(); - expect(tools).toHaveLength(12); + expect(tools).toHaveLength(4); expect(tools.map((t) => t.name)).toEqual([...EXPECTED_TOOL_NAMES]); }); diff --git a/packages/mcp/mcp-server-inventory/README.md b/packages/mcp/mcp-server-inventory/README.md index 626d2d88..4cce07af 100644 --- a/packages/mcp/mcp-server-inventory/README.md +++ b/packages/mcp/mcp-server-inventory/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-inventory/src/cli.ts b/packages/mcp/mcp-server-inventory/src/cli.ts index ea7007df..f52fe597 100644 --- a/packages/mcp/mcp-server-inventory/src/cli.ts +++ b/packages/mcp/mcp-server-inventory/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { InventoryMixin } from './graphql.js'; import { getInventoryTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-inventory', version: '1.0.0', getTools: getInventoryTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new InventoryMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-preferences/README.md b/packages/mcp/mcp-server-preferences/README.md index 31fe93c4..046d29d5 100644 --- a/packages/mcp/mcp-server-preferences/README.md +++ b/packages/mcp/mcp-server-preferences/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-preferences/src/cli.ts b/packages/mcp/mcp-server-preferences/src/cli.ts index 4d0889a2..9a0169e3 100644 --- a/packages/mcp/mcp-server-preferences/src/cli.ts +++ b/packages/mcp/mcp-server-preferences/src/cli.ts @@ -1,9 +1,5 @@ #!/usr/bin/env node -import { - createMCPServer, - TranscendGraphQLBase, - TranscendRestClient, -} from '@transcend-io/mcp-server-core'; +import { createMCPServer, TranscendGraphQLBase } from '@transcend-io/mcp-server-core'; import { getPreferenceTools } from './tools/index.js'; @@ -11,8 +7,7 @@ createMCPServer({ name: 'transcend-mcp-preferences', version: '1.0.0', getTools: getPreferenceTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new TranscendGraphQLBase(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server-preferences/src/index.ts b/packages/mcp/mcp-server-preferences/src/index.ts index 7ebbed92..c8331555 100644 --- a/packages/mcp/mcp-server-preferences/src/index.ts +++ b/packages/mcp/mcp-server-preferences/src/index.ts @@ -1,34 +1 @@ export { getPreferenceTools } from './tools/index.js'; - -export { - IdentifierSchema, - QueryPreferencesSchema, - type IdentifierInput, - type QueryPreferencesInput, -} from './tools/preferences_query.js'; -export { - UpsertPreferencesSchema, - UpsertPreferencesPurposeSchema, - UpsertPreferencesRecordSchema, - type UpsertPreferencesInput, - type UpsertPreferencesPurposeInput, - type UpsertPreferencesRecordInput, -} from './tools/preferences_upsert.js'; -export { - DeletePreferencesSchema, - type DeletePreferencesInput, -} from './tools/preferences_delete.js'; -export { - AppendIdentifiersSchema, - type AppendIdentifiersInput, -} from './tools/preferences_append_identifiers.js'; -export { - UpdateIdentifiersSchema, - UpdateIdentifiersItemSchema, - type UpdateIdentifiersInput, - type UpdateIdentifiersItemInput, -} from './tools/preferences_update_identifiers.js'; -export { - DeleteIdentifiersSchema, - type DeleteIdentifiersInput, -} from './tools/preferences_delete_identifiers.js'; diff --git a/packages/mcp/mcp-server-preferences/src/tools/index.ts b/packages/mcp/mcp-server-preferences/src/tools/index.ts index 6c2a5b24..25ee9f6b 100644 --- a/packages/mcp/mcp-server-preferences/src/tools/index.ts +++ b/packages/mcp/mcp-server-preferences/src/tools/index.ts @@ -1,19 +1,6 @@ import type { ToolDefinition, ToolClients } from '@transcend-io/mcp-server-core'; -import { createPreferencesAppendIdentifiersTool } from './preferences_append_identifiers.js'; -import { createPreferencesDeleteTool } from './preferences_delete.js'; -import { createPreferencesDeleteIdentifiersTool } from './preferences_delete_identifiers.js'; -import { createPreferencesQueryTool } from './preferences_query.js'; -import { createPreferencesUpdateIdentifiersTool } from './preferences_update_identifiers.js'; -import { createPreferencesUpsertTool } from './preferences_upsert.js'; - +// eslint-disable-next-line @typescript-eslint/no-unused-vars export function getPreferenceTools(clients: ToolClients): ToolDefinition[] { - return [ - createPreferencesQueryTool(clients), - createPreferencesUpsertTool(clients), - createPreferencesDeleteTool(clients), - createPreferencesAppendIdentifiersTool(clients), - createPreferencesUpdateIdentifiersTool(clients), - createPreferencesDeleteIdentifiersTool(clients), - ]; + return []; } diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_append_identifiers.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_append_identifiers.ts deleted file mode 100644 index 43bf442d..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_append_identifiers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -import { IdentifierSchema } from './preferences_query.js'; - -export const AppendIdentifiersSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - user_id: z.string().describe('User ID to append identifiers to'), - identifiers: z.array(IdentifierSchema).describe('Array of identifier objects to append'), -}); -export type AppendIdentifiersInput = z.infer; - -export function createPreferencesAppendIdentifiersTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_append_identifiers', - description: 'Append additional identifiers to an existing user preference record', - category: 'Preference Management', - readOnly: false, - confirmationHint: 'Appends identifiers to the user preference record', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, - zodSchema: AppendIdentifiersSchema, - handler: async ({ partition, user_id, identifiers }) => { - const result = await rest.appendIdentifiers( - partition, - user_id, - identifiers.map((id) => ({ - value: id.value, - type: id.type, - })), - ); - - return createToolResult(true, { - ...result, - identifiersAdded: identifiers.length, - message: 'Identifiers appended successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_delete.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_delete.ts deleted file mode 100644 index 60713ad4..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_delete.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -import { IdentifierSchema } from './preferences_query.js'; - -export const DeletePreferencesSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - identifiers: z.array(IdentifierSchema).describe('Array of identifier objects to delete'), -}); -export type DeletePreferencesInput = z.infer; - -export function createPreferencesDeleteTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_delete', - description: 'Delete consent preferences for specified users', - category: 'Preference Management', - readOnly: false, - confirmationHint: 'Deletes preference data for the identifiers', - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }, - zodSchema: DeletePreferencesSchema, - handler: async ({ partition, identifiers }) => { - const result = await rest.deletePreferences( - partition, - identifiers.map((id) => ({ - value: id.value, - type: id.type, - })), - ); - - return createToolResult(true, { - ...result, - message: `Successfully deleted ${result.count} preference records`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_delete_identifiers.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_delete_identifiers.ts deleted file mode 100644 index 3109e9d2..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_delete_identifiers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -import { IdentifierSchema } from './preferences_query.js'; - -export const DeleteIdentifiersSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - user_id: z.string().describe('User ID to delete identifiers from'), - identifiers: z.array(IdentifierSchema).describe('Array of identifier objects to delete'), -}); -export type DeleteIdentifiersInput = z.infer; - -export function createPreferencesDeleteIdentifiersTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_delete_identifiers', - description: 'Delete specific identifiers from a user preference record', - category: 'Preference Management', - readOnly: false, - confirmationHint: 'Deletes identifiers from the user preference record', - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }, - zodSchema: DeleteIdentifiersSchema, - handler: async ({ partition, user_id, identifiers }) => { - const result = await rest.deleteIdentifiers( - partition, - user_id, - identifiers.map((id) => ({ - value: id.value, - type: id.type, - })), - ); - - return createToolResult(true, { - ...result, - identifiersDeleted: identifiers.length, - message: 'Identifiers deleted successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_query.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_query.ts deleted file mode 100644 index 605b22cc..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_query.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createListResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const IdentifierSchema = z.object({ - value: z.string().describe('Identifier value'), - type: z.string().optional().describe('Identifier type (optional)'), -}); -export type IdentifierInput = z.infer; - -export const QueryPreferencesSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - identifiers: z.array(IdentifierSchema).describe('Array of identifier objects to query'), -}); -export type QueryPreferencesInput = z.infer; - -export function createPreferencesQueryTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_query', - description: 'Query consent preferences for multiple users by their identifiers', - category: 'Preference Management', - readOnly: true, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, - zodSchema: QueryPreferencesSchema, - handler: async ({ partition, identifiers }) => { - const result = await rest.queryPreferences({ - partition, - identifiers: identifiers.map((id) => ({ - value: id.value, - type: id.type, - })), - }); - - return createListResult(result, { - totalCount: result.length, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_update_identifiers.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_update_identifiers.ts deleted file mode 100644 index ee66375b..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_update_identifiers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const UpdateIdentifiersItemSchema = z.object({ - oldValue: z.string().describe('Old identifier value'), - newValue: z.string().describe('New identifier value'), - type: z.string().optional().describe('Identifier type (optional)'), -}); -export type UpdateIdentifiersItemInput = z.infer; - -export const UpdateIdentifiersSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - user_id: z.string().describe('User ID to update identifiers for'), - identifiers: z - .array(UpdateIdentifiersItemSchema) - .describe('Array of identifier update objects with old and new values'), -}); -export type UpdateIdentifiersInput = z.infer; - -export function createPreferencesUpdateIdentifiersTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_update_identifiers', - description: 'Update existing identifiers for a user (e.g., when email changes)', - category: 'Preference Management', - readOnly: false, - confirmationHint: 'Updates identifiers for the user preference record', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }, - zodSchema: UpdateIdentifiersSchema, - handler: async ({ partition, user_id, identifiers }) => { - const result = await rest.updateIdentifiers( - partition, - user_id, - identifiers.map((id) => ({ - oldValue: id.oldValue, - newValue: id.newValue, - type: id.type, - })), - ); - - return createToolResult(true, { - ...result, - identifiersUpdated: identifiers.length, - message: 'Identifiers updated successfully', - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/src/tools/preferences_upsert.ts b/packages/mcp/mcp-server-preferences/src/tools/preferences_upsert.ts deleted file mode 100644 index 08ab8510..00000000 --- a/packages/mcp/mcp-server-preferences/src/tools/preferences_upsert.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createToolResult, defineTool, z, type ToolClients } from '@transcend-io/mcp-server-core'; - -export const UpsertPreferencesPurposeSchema = z.object({ - purpose: z.string().describe('Purpose slug'), - enabled: z.boolean().describe('Whether consent is granted'), -}); -export type UpsertPreferencesPurposeInput = z.infer; - -export const UpsertPreferencesRecordSchema = z.object({ - identifier: z.string().describe('User identifier'), - identifierType: z.string().optional().describe('Identifier type (optional)'), - purposes: z.array(UpsertPreferencesPurposeSchema).describe('Array of purpose consent settings'), - confirmed: z.boolean().optional().describe('Whether consent was explicitly confirmed'), -}); -export type UpsertPreferencesRecordInput = z.infer; - -export const UpsertPreferencesSchema = z.object({ - partition: z.string().describe('Partition/organization context'), - records: z.array(UpsertPreferencesRecordSchema).describe('Array of preference records to upsert'), -}); -export type UpsertPreferencesInput = z.infer; - -export function createPreferencesUpsertTool(clients: ToolClients) { - const { rest } = clients; - return defineTool({ - name: 'preferences_upsert', - description: 'Batch upsert consent preference records for multiple users', - category: 'Preference Management', - readOnly: false, - confirmationHint: 'Creates or updates preference records for users', - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }, - zodSchema: UpsertPreferencesSchema, - handler: async ({ partition, records }) => { - const result = await rest.upsertPreferences({ - partition, - records: records.map((record) => ({ - identifier: record.identifier, - identifierType: record.identifierType, - purposes: record.purposes.map((p) => ({ - purpose: p.purpose, - enabled: p.enabled, - })), - confirmed: record.confirmed, - })), - }); - - return createToolResult(true, { - ...result, - recordsProcessed: records.length, - message: `Successfully upserted ${result.count} preference records`, - }); - }, - }); -} diff --git a/packages/mcp/mcp-server-preferences/tests/preferences.test.ts b/packages/mcp/mcp-server-preferences/tests/preferences.test.ts index 2ac1341f..b49eb8a1 100644 --- a/packages/mcp/mcp-server-preferences/tests/preferences.test.ts +++ b/packages/mcp/mcp-server-preferences/tests/preferences.test.ts @@ -1,93 +1,10 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { getPreferenceTools } from '../src/tools.js'; -const EXPECTED_TOOL_NAMES = [ - 'preferences_query', - 'preferences_upsert', - 'preferences_delete', - 'preferences_append_identifiers', - 'preferences_update_identifiers', - 'preferences_delete_identifiers', -] as const; - describe('Preferences Tools', () => { - let mockRest: { - queryPreferences: ReturnType; - upsertPreferences: ReturnType; - deletePreferences: ReturnType; - appendIdentifiers: ReturnType; - updateIdentifiers: ReturnType; - deleteIdentifiers: ReturnType; - }; - - beforeEach(() => { - mockRest = { - queryPreferences: vi.fn(), - upsertPreferences: vi.fn(), - deletePreferences: vi.fn(), - appendIdentifiers: vi.fn(), - updateIdentifiers: vi.fn(), - deleteIdentifiers: vi.fn(), - }; - }); - - const getTools = () => - getPreferenceTools({ - rest: mockRest as never, - graphql: {} as never, - }); - - it('registers exactly 6 tools with expected names', () => { - const tools = getTools(); - expect(tools).toHaveLength(6); - expect(tools.map((t) => t.name)).toEqual([...EXPECTED_TOOL_NAMES]); - }); - - describe('preferences_query', () => { - it('zodSchema rejects input when required fields are missing', () => { - const tools = getTools(); - const tool = tools.find((t) => t.name === 'preferences_query')!; - - const result = tool.zodSchema.safeParse({}); - - expect(result.success).toBe(false); - expect((result as any).error.issues.map((i: any) => i.path[0])).toEqual( - expect.arrayContaining(['partition', 'identifiers']), - ); - }); - - it('returns preferences on success', async () => { - const preferences = [{ userId: 'u1', purposes: [{ purpose: 'analytics', enabled: true }] }]; - mockRest.queryPreferences.mockResolvedValue(preferences); - - const tools = getTools(); - const tool = tools.find((t) => t.name === 'preferences_query')!; - - const result = await tool.handler({ - partition: 'my-org', - identifiers: [{ value: 'user@example.com', type: 'email' }], - }); - - expect(result).toMatchObject({ success: true, data: preferences }); - expect(mockRest.queryPreferences).toHaveBeenCalledWith({ - partition: 'my-org', - identifiers: [{ value: 'user@example.com', type: 'email' }], - }); - }); - - it('throws when client throws', async () => { - mockRest.queryPreferences.mockRejectedValue(new Error('REST error')); - - const tools = getTools(); - const tool = tools.find((t) => t.name === 'preferences_query')!; - - await expect( - tool.handler({ - partition: 'my-org', - identifiers: [{ value: 'user@example.com' }], - }), - ).rejects.toThrow('REST error'); - }); + it('returns an empty array (REST-backed tools removed)', () => { + const tools = getPreferenceTools({ graphql: {} as never }); + expect(tools).toHaveLength(0); }); }); diff --git a/packages/mcp/mcp-server-workflows/README.md b/packages/mcp/mcp-server-workflows/README.md index a3f63dd0..87b78170 100644 --- a/packages/mcp/mcp-server-workflows/README.md +++ b/packages/mcp/mcp-server-workflows/README.md @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. diff --git a/packages/mcp/mcp-server-workflows/src/cli.ts b/packages/mcp/mcp-server-workflows/src/cli.ts index 4f73022b..271e2018 100644 --- a/packages/mcp/mcp-server-workflows/src/cli.ts +++ b/packages/mcp/mcp-server-workflows/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createMCPServer, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { createMCPServer } from '@transcend-io/mcp-server-core'; import { WorkflowsMixin } from './graphql.js'; import { getWorkflowTools } from './tools/index.js'; @@ -8,8 +8,7 @@ createMCPServer({ name: 'transcend-mcp-workflows', version: '1.0.0', getTools: getWorkflowTools, - createClients: (apiKey, sombraUrl, graphqlUrl) => ({ - rest: new TranscendRestClient(apiKey, sombraUrl), + createClients: (apiKey, graphqlUrl) => ({ graphql: new WorkflowsMixin(apiKey, graphqlUrl), }), }); diff --git a/packages/mcp/mcp-server/README.md b/packages/mcp/mcp-server/README.md index 58068bf8..4a517ee3 100644 --- a/packages/mcp/mcp-server/README.md +++ b/packages/mcp/mcp-server/README.md @@ -2,7 +2,7 @@ > **Alpha** — this package is under active development and has not yet been published to npm. APIs may change without notice. -Unified Transcend MCP Server that combines all domain tools into a single server. This is the "everything in one place" option — install this package when you want access to all 70+ Transcend tools at once. +Unified Transcend MCP Server that combines all domain tools into a single server. This is the "everything in one place" option — install this package when you want access to all Transcend tools at once. Requires **Node.js ≥ 22.12** (see `engines` in `package.json`). @@ -29,7 +29,7 @@ The process speaks MCP over **stdio** and is meant to be launched by an MCP clie ### MCP client configuration -`npx` runs the package’s `transcend-mcp` binary (see `bin` in `package.json`). Add to your MCP client config (for example Claude Desktop or Cursor): +`npx` runs the package's `transcend-mcp` binary (see `bin` in `package.json`). Add to your MCP client config (for example Claude Desktop or Cursor): ```json { @@ -45,13 +45,13 @@ The process speaks MCP over **stdio** and is meant to be launched by an MCP clie } ``` -When developing in this repository, reuse the same variable names from root **`secret.env`** in the `env` block, or use your client’s env-file support if it has one. +When developing in this repository, reuse the same variable names from root **`secret.env`** in the `env` block, or use your client's env-file support if it has one. ### Run from the monorepo 1. **Credentials** — From the repository root, copy [`secret.env.example`](../../../secret.env.example) to **`secret.env`** and set `TRANSCEND_API_KEY` (and optional URL overrides). -2. **Build and run** — `node ./dist/cli.mjs` matches the `transcend-mcp` `bin` (use `node` because `pnpm exec transcend-mcp` may not resolve this package’s own binary in a pnpm workspace): +2. **Build and run** — `node ./dist/cli.mjs` matches the `transcend-mcp` `bin` (use `node` because `pnpm exec transcend-mcp` may not resolve this package's own binary in a pnpm workspace): ```bash # from the repository root — builds the unified server, all domain packages, and mcp-server-core @@ -66,11 +66,10 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout ### Environment variables -| Variable | Required | Default | Description | -| ----------------------- | -------- | ------------------------------------------ | ----------------- | -| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | -| `TRANSCEND_API_URL` | No | `https://multi-tenant.sombra.transcend.io` | Sombra API URL | -| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | +| Variable | Required | Default | Description | +| ----------------------- | -------- | -------------------------- | ----------------- | +| `TRANSCEND_API_KEY` | Yes | — | Transcend API key | +| `TRANSCEND_GRAPHQL_URL` | No | `https://api.transcend.io` | GraphQL API URL | **Monorepo:** keep these in root **`secret.env`** (from [`secret.env.example`](../../../secret.env.example)); see **Run from the monorepo**. @@ -78,7 +77,7 @@ See [CONTRIBUTING.md](../../../CONTRIBUTING.md#mcp-servers) for workspace layout This package composes all domain MCP packages via `ToolRegistry`, which aggregates tools from each domain (`getConsentTools`, `getDSRTools`, etc.) into a single tool namespace. A composed `TranscendGraphQLClient` mixes in all domain GraphQL capabilities so each tool has access to the API surface it needs. -If 70+ tools is too many for your AI agent, install individual domain packages instead — see the [MCP section of the root README](../../../README.md#mcp-servers). +If the tool count is too many for your AI agent, install individual domain packages instead — see the [MCP section of the root README](../../../README.md#mcp-servers). ## Related packages diff --git a/packages/mcp/mcp-server/src/cli.ts b/packages/mcp/mcp-server/src/cli.ts index 112c61ff..7f04967f 100644 --- a/packages/mcp/mcp-server/src/cli.ts +++ b/packages/mcp/mcp-server/src/cli.ts @@ -3,7 +3,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { SimpleLogger, TranscendRestClient } from '@transcend-io/mcp-server-core'; +import { SimpleLogger } from '@transcend-io/mcp-server-core'; import { TranscendGraphQLClient } from './graphql-client.js'; import { ToolRegistry } from './registry.js'; @@ -13,7 +13,6 @@ const VERSION = '3.0.2'; const logger = new SimpleLogger(); const apiKey = process.env.TRANSCEND_API_KEY; -const sombraUrl = process.env.TRANSCEND_API_URL || 'https://multi-tenant.sombra.transcend.io'; const graphqlUrl = process.env.TRANSCEND_GRAPHQL_URL || 'https://api.transcend.io'; if (!apiKey) { @@ -21,13 +20,11 @@ if (!apiKey) { process.exit(1); } -logger.info('Initializing Transcend API clients...', { sombraUrl, graphqlUrl }); +logger.info('Initializing Transcend API clients...', { graphqlUrl }); -const restClient = new TranscendRestClient(apiKey, sombraUrl); const graphqlClient = new TranscendGraphQLClient(apiKey, graphqlUrl); const toolRegistry = new ToolRegistry({ - rest: restClient, graphql: graphqlClient, }); @@ -95,7 +92,6 @@ async function main(): Promise { await server.connect(transport); logger.info('Transcend MCP Server started successfully', { - sombraUrl, graphqlUrl, tools: toolRegistry.getToolCount(), }); diff --git a/packages/mcp/mcp-server/src/graphql-client.ts b/packages/mcp/mcp-server/src/graphql-client.ts index 5d1ea149..55165fb3 100644 --- a/packages/mcp/mcp-server/src/graphql-client.ts +++ b/packages/mcp/mcp-server/src/graphql-client.ts @@ -35,9 +35,6 @@ export class TranscendGraphQLClient extends TranscendGraphQLBase { // DSR declare listRequests: InstanceType['listRequests']; declare getRequest: InstanceType['getRequest']; - declare employeeMakeDataSubjectRequest: InstanceType< - typeof DSRMixin - >['employeeMakeDataSubjectRequest']; declare cancelRequest: InstanceType['cancelRequest']; // Inventory diff --git a/packages/mcp/mcp-server/src/registry.ts b/packages/mcp/mcp-server/src/registry.ts index 2d062c75..5c0e8d12 100644 --- a/packages/mcp/mcp-server/src/registry.ts +++ b/packages/mcp/mcp-server/src/registry.ts @@ -2,11 +2,7 @@ import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-sc import { getAdminTools } from '@transcend-io/mcp-server-admin'; import { getAssessmentTools } from '@transcend-io/mcp-server-assessments'; import { getConsentTools } from '@transcend-io/mcp-server-consent'; -import { - createErrorResult, - type ToolDefinition, - type TranscendRestClient, -} from '@transcend-io/mcp-server-core'; +import { createErrorResult, type ToolDefinition } from '@transcend-io/mcp-server-core'; import { getDiscoveryTools } from '@transcend-io/mcp-server-discovery'; import { getDSRTools } from '@transcend-io/mcp-server-dsr'; import { getInventoryTools } from '@transcend-io/mcp-server-inventory'; @@ -16,8 +12,6 @@ import { getWorkflowTools } from '@transcend-io/mcp-server-workflows'; import type { TranscendGraphQLClient } from './graphql-client.js'; export interface UmbrellaToolClients { - /** REST client for Sombra API */ - rest: TranscendRestClient; /** Composed GraphQL client with all domain mixins */ graphql: TranscendGraphQLClient; } diff --git a/packages/mcp/mcp-server/tests/annotations.test.ts b/packages/mcp/mcp-server/tests/annotations.test.ts index c6cad4e8..58203a04 100644 --- a/packages/mcp/mcp-server/tests/annotations.test.ts +++ b/packages/mcp/mcp-server/tests/annotations.test.ts @@ -14,7 +14,6 @@ import { EXPECTED_UMBRELLA_TOOL_COUNT } from './umbrella-tool-count.js'; const stubFn = () => vi.fn(); const mockClients: ToolClients = { - rest: new Proxy({} as ToolClients['rest'], { get: stubFn }), graphql: new Proxy({} as ToolClients['graphql'], { get: stubFn }), }; @@ -79,8 +78,6 @@ describe('MCP Tool Annotations', () => { 'dsr_cancel', 'admin_create_api_key', 'inventory_create_data_silo', - 'preferences_delete', - 'preferences_delete_identifiers', 'assessments_submit_response', ]; @@ -101,14 +98,10 @@ describe('MCP Tool Annotations', () => { describe('idempotent mutative tools are annotated correctly', () => { const expectedIdempotentMutative = [ 'workflows_update_config', - 'consent_set_preferences', - 'preferences_upsert', - 'preferences_update_identifiers', 'inventory_update_data_silo', 'assessments_update', 'assessments_update_assignees', 'assessments_answer_question', - 'dsr_respond_erasure', ]; it.each(expectedIdempotentMutative)( diff --git a/packages/mcp/mcp-server/tests/registry.test.ts b/packages/mcp/mcp-server/tests/registry.test.ts index 2cf66997..6698c368 100644 --- a/packages/mcp/mcp-server/tests/registry.test.ts +++ b/packages/mcp/mcp-server/tests/registry.test.ts @@ -1,7 +1,7 @@ import { getAdminTools } from '@transcend-io/mcp-server-admin'; import { getAssessmentTools } from '@transcend-io/mcp-server-assessments'; import { getConsentTools } from '@transcend-io/mcp-server-consent'; -import { TranscendRestClient, type ToolClients } from '@transcend-io/mcp-server-core'; +import type { ToolClients } from '@transcend-io/mcp-server-core'; import { getDiscoveryTools } from '@transcend-io/mcp-server-discovery'; import { getDSRTools } from '@transcend-io/mcp-server-dsr'; import { getInventoryTools } from '@transcend-io/mcp-server-inventory'; @@ -16,7 +16,6 @@ import { EXPECTED_UMBRELLA_TOOL_COUNT } from './umbrella-tool-count.js'; const stubFn = () => vi.fn(); const mockClients: ToolClients = { - rest: new Proxy({} as ToolClients['rest'], { get: stubFn }), graphql: new Proxy({} as ToolClients['graphql'], { get: stubFn }), }; @@ -41,18 +40,16 @@ describe('ToolRegistry', () => { }); it('ToolRegistry registers all tools with correct count', () => { - const rest = new TranscendRestClient('test-key', 'http://localhost:0'); const graphql = new TranscendGraphQLClient('test-key', 'http://localhost:0'); - const registry = new ToolRegistry({ rest, graphql }); + const registry = new ToolRegistry({ graphql }); expect(registry.getToolCount()).toBe(EXPECTED_UMBRELLA_TOOL_COUNT); expect(registry.getToolList()).toHaveLength(EXPECTED_UMBRELLA_TOOL_COUNT); }); it('getToolList returns well-formed tool descriptors', () => { - const rest = new TranscendRestClient('test-key', 'http://localhost:0'); const graphql = new TranscendGraphQLClient('test-key', 'http://localhost:0'); - const registry = new ToolRegistry({ rest, graphql }); + const registry = new ToolRegistry({ graphql }); for (const tool of registry.getToolList()) { expect(tool.name).toBeTruthy(); @@ -63,17 +60,15 @@ describe('ToolRegistry', () => { }); it('getTool returns undefined for unknown tools', () => { - const rest = new TranscendRestClient('test-key', 'http://localhost:0'); const graphql = new TranscendGraphQLClient('test-key', 'http://localhost:0'); - const registry = new ToolRegistry({ rest, graphql }); + const registry = new ToolRegistry({ graphql }); expect(registry.getTool('nonexistent_tool')).toBeUndefined(); }); it('executeTool throws for unknown tools', async () => { - const rest = new TranscendRestClient('test-key', 'http://localhost:0'); const graphql = new TranscendGraphQLClient('test-key', 'http://localhost:0'); - const registry = new ToolRegistry({ rest, graphql }); + const registry = new ToolRegistry({ graphql }); await expect(registry.executeTool('nonexistent_tool', {})).rejects.toThrow('Unknown tool'); }); diff --git a/packages/mcp/mcp-server/tests/umbrella-tool-count.ts b/packages/mcp/mcp-server/tests/umbrella-tool-count.ts index 17d7fb39..d2164cd0 100644 --- a/packages/mcp/mcp-server/tests/umbrella-tool-count.ts +++ b/packages/mcp/mcp-server/tests/umbrella-tool-count.ts @@ -2,4 +2,4 @@ * Single source of truth for the expected number of tools in the umbrella server. * Update this constant when tools are added or removed from any domain package. */ -export const EXPECTED_UMBRELLA_TOOL_COUNT = 70; +export const EXPECTED_UMBRELLA_TOOL_COUNT = 52;