Compare commits

..

4 Commits

Author SHA1 Message Date
czlonkowski
e52a04a291 fix: return 202 for notifications with stale sessions, warn on updateTable columns (#654)
**Issue #654 (critical):** handleRequest() returned HTTP 400 for JSON-RPC
notifications with stale/expired session IDs. Per JSON-RPC 2.0 spec,
notifications don't expect responses. Returning 400 caused Claude's proxy
to interpret the connection as broken, triggering reconnection storms
(930 errors/day, 216 users affected).

Fix: Added isJsonRpcNotification() helper that detects single and batch
notifications (messages with method but no id field). Path C in
handleRequest() now returns 202 Accepted for notifications with stale
sessions instead of 400. Also added TOCTOU null guard in Path B.

**Issue 1 (medium):** updateTable silently accepted columns parameter
via the shared MCP tool schema. Zod stripped it but gave no feedback.

Fix: handleUpdateTable now detects columns in raw args and appends a
warning to the response. Tool schema descriptions clarified that columns
are for createTable only and schema is immutable after creation.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:21:29 +01:00
Romuald Członkowski
6f6668acc4 fix: resolve double URL-encoding in datatable filter/sortBy query params (#652) 2026-03-22 15:59:34 +01:00
Romuald Członkowski
c5665632af fix: resolve 5 bugs in n8n_manage_datatable (#651) 2026-03-22 00:12:39 +01:00
Romuald Członkowski
be3d07dbdc feat: add n8n_manage_datatable MCP tool with full CRUD (#640) (#650)
* feat: add n8n_create_data_table MCP tool and projectId for create workflow (#640)

Add new MCP tool to create n8n data tables via the REST API:
- n8n_create_data_table tool definition with name + columns schema
- handleCreateDataTable handler with Zod validation and N8nApiError handling
- N8nApiClient.createDataTable() calling POST /data-tables
- DataTable, DataTableColumn, DataTableColumnResponse types per OpenAPI spec
- Column types: string | number | boolean | date | json
- Input validation: .min(1) on table name and column names
- Tool documentation with examples, use cases, and pitfalls

Also adds projectId parameter to n8n_create_workflow for enterprise
project support, and fixes stale management tool count in health check.

Based on work by @djakielski in PR #646.
Co-Authored-By: Dominik Jakielski <dominik.jakielski@urlaubsguru.de>

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: replace n8n_create_data_table with n8n_manage_datatable (10 actions)

Replaces the single-purpose n8n_create_data_table tool with a comprehensive
n8n_manage_datatable tool covering all 10 n8n data table API endpoints:

Table operations: createTable, listTables, getTable, updateTable, deleteTable
Row operations: getRows, insertRows, updateRows, upsertRows, deleteRows

- Filter system with and/or logic and 8 condition operators
- Dry-run support for updateRows, upsertRows, deleteRows
- Pagination, sorting, and full-text search for row listing
- 9 new N8nApiClient methods for all data table endpoints
- Shared error handler and consolidated Zod schemas
- Comprehensive tool documentation with examples per action
- 36 handler tests + 18 API client tests

BREAKING: n8n_create_data_table removed. Use n8n_manage_datatable with
action="createTable" instead.

Based on work by @djakielski in PR #646.
Co-Authored-By: Dominik Jakielski <dominik.jakielski@urlaubsguru.de>

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
2026-03-21 19:06:22 +01:00
44 changed files with 2550 additions and 46 deletions

View File

@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.40.3] - 2026-03-22
### Fixed
- **Notification 400 disconnect storms (#654)**: `handleRequest()` now returns 202 Accepted for JSON-RPC notifications with stale/expired session IDs instead of 400. Per JSON-RPC 2.0 spec, notifications don't expect responses — returning 400 caused Claude's proxy to trigger reconnection storms (930 errors/day, 216 users affected)
- **TOCTOU race in session lookup**: Added null guard after transport assignment to handle sessions removed between the existence check and use
- **`updateTable` silently ignoring `columns` parameter**: Now returns a warning message when `columns` is passed to `updateTable`, clarifying that table schema is immutable after creation via the public API
- **Tool schema descriptions clarified**: `name` and `columns` parameter descriptions now explicitly document that `updateTable` is rename-only and columns are for `createTable` only
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.40.2] - 2026-03-22
### Fixed
- **Double URL-encoding of `filter` and `sortBy` in `getRows`/`deleteRows`**: Moved `encodeURIComponent()` from handler layer to a custom `paramsSerializer` in the API client. Handlers were encoding values before passing them as Axios params, causing double-encoding (`%257B` instead of `%7B`). Handlers now pass raw values; the API client encodes once via `serializeDataTableParams()`
- **`updateTable` documentation clarified**: Explicitly notes that only renaming is supported (no column modifications via public API)
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.40.1] - 2026-03-21
### Fixed
- **`n8n_manage_datatable` row operations broken by MCP transport serialization**: `data` parameter received as string instead of JSON — added `z.preprocess` coercers for array/object/filter params
- **`n8n_manage_datatable` filter/sortBy URL encoding**: n8n API requires URL-encoded query params — added `encodeURIComponent()` for filter and sortBy in getRows and deleteRows (revised in 2.40.2 to move encoding to API client layer)
- **`json` column type rejected by n8n API**: Removed `json` from column type enum (n8n only accepts string/number/boolean/date)
- **Garbled 404 error messages**: Fixed `N8nNotFoundError` constructor — API error messages are now passed through cleanly instead of being wrapped in "Resource with ID ... not found"
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.40.0] - 2026-03-21
### Changed
- **`n8n_manage_datatable` MCP tool** (replaces `n8n_create_data_table`): Full data table management covering all 10 n8n data table API endpoints
- **Table operations**: createTable, listTables, getTable, updateTable, deleteTable
- **Row operations**: getRows, insertRows, updateRows, upsertRows, deleteRows
- Filter system with and/or logic and 8 condition operators (eq, neq, like, ilike, gt, gte, lt, lte)
- Dry-run support for updateRows, upsertRows, deleteRows
- Pagination, sorting, and full-text search for row listing
- Shared error handler and consolidated Zod schemas for consistency
- 9 new `N8nApiClient` methods for all data table endpoints
- **`projectId` parameter for `n8n_create_workflow`**: Create workflows directly in a specific team project (enterprise feature)
### Breaking
- `n8n_create_data_table` tool replaced by `n8n_manage_datatable` with `action: "createTable"`
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.38.0] - 2026-03-20
### Added

View File

@@ -26,4 +26,14 @@ export declare function handleDiagnostic(request: any, context?: InstanceContext
export declare function handleWorkflowVersions(args: unknown, repository: NodeRepository, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleDeployTemplate(args: unknown, templateService: TemplateService, repository: NodeRepository, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleTriggerWebhookWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleCreateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleListTables(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleGetTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleUpdateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleDeleteTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleGetRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleInsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleUpdateRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleUpsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
export declare function handleDeleteRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse>;
//# sourceMappingURL=handlers-n8n-manager.d.ts.map

View File

@@ -1 +1 @@
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}

View File

@@ -56,6 +56,16 @@ exports.handleDiagnostic = handleDiagnostic;
exports.handleWorkflowVersions = handleWorkflowVersions;
exports.handleDeployTemplate = handleDeployTemplate;
exports.handleTriggerWebhookWorkflow = handleTriggerWebhookWorkflow;
exports.handleCreateTable = handleCreateTable;
exports.handleListTables = handleListTables;
exports.handleGetTable = handleGetTable;
exports.handleUpdateTable = handleUpdateTable;
exports.handleDeleteTable = handleDeleteTable;
exports.handleGetRows = handleGetRows;
exports.handleInsertRows = handleInsertRows;
exports.handleUpdateRows = handleUpdateRows;
exports.handleUpsertRows = handleUpsertRows;
exports.handleDeleteRows = handleDeleteRows;
const n8n_api_client_1 = require("../services/n8n-api-client");
const n8n_api_1 = require("../config/n8n-api");
const n8n_api_2 = require("../types/n8n-api");
@@ -175,6 +185,7 @@ const createWorkflowSchema = zod_1.z.object({
executionTimeout: zod_1.z.number().optional(),
errorWorkflow: zod_1.z.string().optional(),
}).optional(),
projectId: zod_1.z.string().optional(),
});
const updateWorkflowSchema = zod_1.z.object({
id: zod_1.z.string(),
@@ -1470,7 +1481,7 @@ async function handleDiagnostic(request, context) {
}
}
const documentationTools = 7;
const managementTools = apiConfigured ? 13 : 0;
const managementTools = apiConfigured ? 14 : 0;
const totalTools = documentationTools + managementTools;
const versionCheck = await (0, npm_version_checker_1.checkNpmVersion)();
const cacheMetricsData = getInstanceCacheMetrics();
@@ -2038,4 +2049,241 @@ async function handleTriggerWebhookWorkflow(args, context) {
};
}
}
const dataTableFilterConditionSchema = zod_1.z.object({
columnName: zod_1.z.string().min(1),
condition: zod_1.z.enum(['eq', 'neq', 'like', 'ilike', 'gt', 'gte', 'lt', 'lte']),
value: zod_1.z.any(),
});
const dataTableFilterSchema = zod_1.z.object({
type: zod_1.z.enum(['and', 'or']).optional().default('and'),
filters: zod_1.z.array(dataTableFilterConditionSchema).min(1, 'At least one filter condition is required'),
});
const tableIdSchema = zod_1.z.object({
tableId: zod_1.z.string().min(1, 'tableId is required'),
});
const createTableSchema = zod_1.z.object({
name: zod_1.z.string().min(1, 'Table name cannot be empty'),
columns: zod_1.z.array(zod_1.z.object({
name: zod_1.z.string().min(1, 'Column name cannot be empty'),
type: zod_1.z.enum(['string', 'number', 'boolean', 'date']).optional(),
})).optional(),
});
const listTablesSchema = zod_1.z.object({
limit: zod_1.z.number().min(1).max(100).optional(),
cursor: zod_1.z.string().optional(),
});
const updateTableSchema = tableIdSchema.extend({
name: zod_1.z.string().min(1, 'New table name cannot be empty'),
});
function tryParseJson(val) {
if (typeof val !== 'string')
return val;
try {
return JSON.parse(val);
}
catch {
return val;
}
}
const coerceJsonArray = zod_1.z.preprocess(tryParseJson, zod_1.z.array(zod_1.z.record(zod_1.z.unknown())));
const coerceJsonObject = zod_1.z.preprocess(tryParseJson, zod_1.z.record(zod_1.z.unknown()));
const coerceJsonFilter = zod_1.z.preprocess(tryParseJson, dataTableFilterSchema);
const getRowsSchema = tableIdSchema.extend({
limit: zod_1.z.number().min(1).max(100).optional(),
cursor: zod_1.z.string().optional(),
filter: zod_1.z.union([coerceJsonFilter, zod_1.z.string()]).optional(),
sortBy: zod_1.z.string().optional(),
search: zod_1.z.string().optional(),
});
const insertRowsSchema = tableIdSchema.extend({
data: coerceJsonArray.pipe(zod_1.z.array(zod_1.z.record(zod_1.z.unknown())).min(1, 'At least one row is required')),
returnType: zod_1.z.enum(['count', 'id', 'all']).optional(),
});
const mutateRowsSchema = tableIdSchema.extend({
filter: coerceJsonFilter,
data: coerceJsonObject,
returnData: zod_1.z.boolean().optional(),
dryRun: zod_1.z.boolean().optional(),
});
const deleteRowsSchema = tableIdSchema.extend({
filter: coerceJsonFilter,
returnData: zod_1.z.boolean().optional(),
dryRun: zod_1.z.boolean().optional(),
});
function handleDataTableError(error) {
if (error instanceof zod_1.z.ZodError) {
return { success: false, error: 'Invalid input', details: { errors: error.errors } };
}
if (error instanceof n8n_errors_1.N8nApiError) {
return {
success: false,
error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error),
code: error.code,
details: error.details,
};
}
return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' };
}
async function handleCreateTable(args, context) {
try {
const client = ensureApiConfigured(context);
const input = createTableSchema.parse(args);
const dataTable = await client.createDataTable(input);
if (!dataTable || !dataTable.id) {
return { success: false, error: 'Data table creation failed: n8n API returned an empty or invalid response' };
}
return {
success: true,
data: { id: dataTable.id, name: dataTable.name },
message: `Data table "${dataTable.name}" created with ID: ${dataTable.id}`,
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleListTables(args, context) {
try {
const client = ensureApiConfigured(context);
const input = listTablesSchema.parse(args || {});
const result = await client.listDataTables(input);
return {
success: true,
data: {
tables: result.data,
count: result.data.length,
nextCursor: result.nextCursor || undefined,
},
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleGetTable(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId } = tableIdSchema.parse(args);
const dataTable = await client.getDataTable(tableId);
return { success: true, data: dataTable };
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleUpdateTable(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, name } = updateTableSchema.parse(args);
const dataTable = await client.updateDataTable(tableId, { name });
return {
success: true,
data: dataTable,
message: `Data table renamed to "${dataTable.name}"`,
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleDeleteTable(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId } = tableIdSchema.parse(args);
await client.deleteDataTable(tableId);
return { success: true, message: `Data table ${tableId} deleted successfully` };
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleGetRows(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, filter, sortBy, ...params } = getRowsSchema.parse(args);
const queryParams = { ...params };
if (filter) {
queryParams.filter = typeof filter === 'string' ? filter : JSON.stringify(filter);
}
if (sortBy) {
queryParams.sortBy = sortBy;
}
const result = await client.getDataTableRows(tableId, queryParams);
return {
success: true,
data: {
rows: result.data,
count: result.data.length,
nextCursor: result.nextCursor || undefined,
},
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleInsertRows(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = insertRowsSchema.parse(args);
const result = await client.insertDataTableRows(tableId, params);
return {
success: true,
data: result,
message: `Rows inserted into data table ${tableId}`,
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleUpdateRows(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = mutateRowsSchema.parse(args);
const result = await client.updateDataTableRows(tableId, params);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: rows matched (no changes applied)' : 'Rows updated successfully',
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleUpsertRows(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = mutateRowsSchema.parse(args);
const result = await client.upsertDataTableRow(tableId, params);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: upsert previewed (no changes applied)' : 'Row upserted successfully',
};
}
catch (error) {
return handleDataTableError(error);
}
}
async function handleDeleteRows(args, context) {
try {
const client = ensureApiConfigured(context);
const { tableId, filter, ...params } = deleteRowsSchema.parse(args);
const queryParams = {
filter: JSON.stringify(filter),
...params,
};
const result = await client.deleteDataTableRows(tableId, queryParams);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: rows matched for deletion (no changes applied)' : 'Rows deleted successfully',
};
}
catch (error) {
return handleDataTableError(error);
}
}
//# sourceMappingURL=handlers-n8n-manager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAmGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IA8GvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA0XrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;IAiF7B,OAAO,CAAC,2BAA2B;YA0UrB,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAmGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IA8GvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA0XrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IA0E1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;IAiF7B,OAAO,CAAC,2BAA2B;YA8VrB,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}

23
dist/mcp/server.js vendored
View File

@@ -720,6 +720,11 @@ class N8NDocumentationMCPServer {
? { valid: true, errors: [] }
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
break;
case 'n8n_manage_datatable':
validationResult = args.action
? { valid: true, errors: [] }
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
break;
case 'n8n_deploy_template':
validationResult = args.templateId !== undefined
? { valid: true, errors: [] }
@@ -1109,6 +1114,24 @@ class N8NDocumentationMCPServer {
if (!this.repository)
throw new Error('Repository not initialized');
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
case 'n8n_manage_datatable': {
this.validateToolParams(name, args, ['action']);
const dtAction = args.action;
switch (dtAction) {
case 'createTable': return n8nHandlers.handleCreateTable(args, this.instanceContext);
case 'listTables': return n8nHandlers.handleListTables(args, this.instanceContext);
case 'getTable': return n8nHandlers.handleGetTable(args, this.instanceContext);
case 'updateTable': return n8nHandlers.handleUpdateTable(args, this.instanceContext);
case 'deleteTable': return n8nHandlers.handleDeleteTable(args, this.instanceContext);
case 'getRows': return n8nHandlers.handleGetRows(args, this.instanceContext);
case 'insertRows': return n8nHandlers.handleInsertRows(args, this.instanceContext);
case 'updateRows': return n8nHandlers.handleUpdateRows(args, this.instanceContext);
case 'upsertRows': return n8nHandlers.handleUpsertRows(args, this.instanceContext);
case 'deleteRows': return n8nHandlers.handleDeleteRows(args, this.instanceContext);
default:
throw new Error(`Unknown action: ${dtAction}. Valid actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows`);
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/mcp/tool-docs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AA4B5C,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAmChE,CAAC;AAGF,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/mcp/tool-docs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AA6B5C,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAoChE,CAAC;AAGF,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}

View File

@@ -29,6 +29,7 @@ exports.toolsDocumentation = {
n8n_test_workflow: workflow_management_1.n8nTestWorkflowDoc,
n8n_executions: workflow_management_1.n8nExecutionsDoc,
n8n_workflow_versions: workflow_management_1.n8nWorkflowVersionsDoc,
n8n_deploy_template: workflow_management_1.n8nDeployTemplateDoc
n8n_deploy_template: workflow_management_1.n8nDeployTemplateDoc,
n8n_manage_datatable: workflow_management_1.n8nManageDatatableDoc
};
//# sourceMappingURL=index.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mcp/tool-docs/index.ts"],"names":[],"mappings":";;;AAGA,2CAA6C;AAC7C,mDAA6C;AAC7C,6CAAoE;AACpE,2CAAiE;AACjE,qCAGkB;AAClB,qCAAyC;AACzC,+DAa+B;AAGlB,QAAA,kBAAkB,GAAsC;IAEnE,mBAAmB,EAAE,8BAAqB;IAC1C,gBAAgB,EAAE,0BAAiB;IAGnC,eAAe,EAAE,sBAAa;IAG9B,YAAY,EAAE,0BAAc;IAG5B,QAAQ,EAAE,0BAAU;IAGpB,aAAa,EAAE,4BAAe;IAC9B,iBAAiB,EAAE,gCAAmB;IAGtC,YAAY,EAAE,0BAAc;IAC5B,gBAAgB,EAAE,8BAAkB;IAGpC,mBAAmB,EAAE,0CAAoB;IACzC,gBAAgB,EAAE,uCAAiB;IACnC,wBAAwB,EAAE,8CAAwB;IAClD,2BAA2B,EAAE,iDAA2B;IACxD,mBAAmB,EAAE,0CAAoB;IACzC,kBAAkB,EAAE,yCAAmB;IACvC,qBAAqB,EAAE,4CAAsB;IAC7C,oBAAoB,EAAE,2CAAqB;IAC3C,iBAAiB,EAAE,wCAAkB;IACrC,cAAc,EAAE,sCAAgB;IAChC,qBAAqB,EAAE,4CAAsB;IAC7C,mBAAmB,EAAE,0CAAoB;CAC1C,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mcp/tool-docs/index.ts"],"names":[],"mappings":";;;AAGA,2CAA6C;AAC7C,mDAA6C;AAC7C,6CAAoE;AACpE,2CAAiE;AACjE,qCAGkB;AAClB,qCAAyC;AACzC,+DAc+B;AAGlB,QAAA,kBAAkB,GAAsC;IAEnE,mBAAmB,EAAE,8BAAqB;IAC1C,gBAAgB,EAAE,0BAAiB;IAGnC,eAAe,EAAE,sBAAa;IAG9B,YAAY,EAAE,0BAAc;IAG5B,QAAQ,EAAE,0BAAU;IAGpB,aAAa,EAAE,4BAAe;IAC9B,iBAAiB,EAAE,gCAAmB;IAGtC,YAAY,EAAE,0BAAc;IAC5B,gBAAgB,EAAE,8BAAkB;IAGpC,mBAAmB,EAAE,0CAAoB;IACzC,gBAAgB,EAAE,uCAAiB;IACnC,wBAAwB,EAAE,8CAAwB;IAClD,2BAA2B,EAAE,iDAA2B;IACxD,mBAAmB,EAAE,0CAAoB;IACzC,kBAAkB,EAAE,yCAAmB;IACvC,qBAAqB,EAAE,4CAAsB;IAC7C,oBAAoB,EAAE,2CAAqB;IAC3C,iBAAiB,EAAE,wCAAkB;IACrC,cAAc,EAAE,sCAAgB;IAChC,qBAAqB,EAAE,4CAAsB;IAC7C,mBAAmB,EAAE,0CAAoB;IACzC,oBAAoB,EAAE,2CAAqB;CAC5C,CAAC"}

View File

@@ -10,4 +10,5 @@ export { n8nTestWorkflowDoc } from './n8n-test-workflow';
export { n8nExecutionsDoc } from './n8n-executions';
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
export { n8nManageDatatableDoc } from './n8n-manage-datatable';
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}

View File

@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.n8nDeployTemplateDoc = exports.n8nWorkflowVersionsDoc = exports.n8nExecutionsDoc = exports.n8nTestWorkflowDoc = exports.n8nAutofixWorkflowDoc = exports.n8nValidateWorkflowDoc = exports.n8nListWorkflowsDoc = exports.n8nDeleteWorkflowDoc = exports.n8nUpdatePartialWorkflowDoc = exports.n8nUpdateFullWorkflowDoc = exports.n8nGetWorkflowDoc = exports.n8nCreateWorkflowDoc = void 0;
exports.n8nManageDatatableDoc = exports.n8nDeployTemplateDoc = exports.n8nWorkflowVersionsDoc = exports.n8nExecutionsDoc = exports.n8nTestWorkflowDoc = exports.n8nAutofixWorkflowDoc = exports.n8nValidateWorkflowDoc = exports.n8nListWorkflowsDoc = exports.n8nDeleteWorkflowDoc = exports.n8nUpdatePartialWorkflowDoc = exports.n8nUpdateFullWorkflowDoc = exports.n8nGetWorkflowDoc = exports.n8nCreateWorkflowDoc = void 0;
var n8n_create_workflow_1 = require("./n8n-create-workflow");
Object.defineProperty(exports, "n8nCreateWorkflowDoc", { enumerable: true, get: function () { return n8n_create_workflow_1.n8nCreateWorkflowDoc; } });
var n8n_get_workflow_1 = require("./n8n-get-workflow");
@@ -25,4 +25,6 @@ var n8n_workflow_versions_1 = require("./n8n-workflow-versions");
Object.defineProperty(exports, "n8nWorkflowVersionsDoc", { enumerable: true, get: function () { return n8n_workflow_versions_1.n8nWorkflowVersionsDoc; } });
var n8n_deploy_template_1 = require("./n8n-deploy-template");
Object.defineProperty(exports, "n8nDeployTemplateDoc", { enumerable: true, get: function () { return n8n_deploy_template_1.n8nDeployTemplateDoc; } });
var n8n_manage_datatable_1 = require("./n8n-manage-datatable");
Object.defineProperty(exports, "n8nManageDatatableDoc", { enumerable: true, get: function () { return n8n_manage_datatable_1.n8nManageDatatableDoc; } });
//# sourceMappingURL=index.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/index.ts"],"names":[],"mappings":";;;AAAA,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA;AAC7B,uDAAuD;AAA9C,qHAAA,iBAAiB,OAAA;AAC1B,uEAAsE;AAA7D,oIAAA,wBAAwB,OAAA;AACjC,6EAA4E;AAAnE,0IAAA,2BAA2B,OAAA;AACpC,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,yHAAA,mBAAmB,OAAA;AAC5B,iEAAiE;AAAxD,+HAAA,sBAAsB,OAAA;AAC/B,+DAA+D;AAAtD,6HAAA,qBAAqB,OAAA;AAC9B,yDAAyD;AAAhD,uHAAA,kBAAkB,OAAA;AAC3B,mDAAoD;AAA3C,kHAAA,gBAAgB,OAAA;AACzB,iEAAiE;AAAxD,+HAAA,sBAAsB,OAAA;AAC/B,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/index.ts"],"names":[],"mappings":";;;AAAA,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA;AAC7B,uDAAuD;AAA9C,qHAAA,iBAAiB,OAAA;AAC1B,uEAAsE;AAA7D,oIAAA,wBAAwB,OAAA;AACjC,6EAA4E;AAAnE,0IAAA,2BAA2B,OAAA;AACpC,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,yHAAA,mBAAmB,OAAA;AAC5B,iEAAiE;AAAxD,+HAAA,sBAAsB,OAAA;AAC/B,+DAA+D;AAAtD,6HAAA,qBAAqB,OAAA;AAC9B,yDAAyD;AAAhD,uHAAA,kBAAkB,OAAA;AAC3B,mDAAoD;AAA3C,kHAAA,gBAAgB,OAAA;AACzB,iEAAiE;AAAxD,+HAAA,sBAAsB,OAAA;AAC/B,6DAA6D;AAApD,2HAAA,oBAAoB,OAAA;AAC7B,+DAA+D;AAAtD,6HAAA,qBAAqB,OAAA"}

View File

@@ -1 +1 @@
{"version":3,"file":"tools-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/tools-n8n-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ1C,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAqlB9C,CAAC"}
{"version":3,"file":"tools-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/tools-n8n-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ1C,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAwoB9C,CAAC"}

View File

@@ -57,6 +57,10 @@ exports.n8nManagementTools = [
executionTimeout: { type: 'number' },
errorWorkflow: { type: 'string' }
}
},
projectId: {
type: 'string',
description: 'Optional project ID to create the workflow in (enterprise feature)'
}
},
required: ['name', 'nodes', 'connections']
@@ -583,6 +587,53 @@ exports.n8nManagementTools = [
destructiveHint: false,
openWorldHint: true,
},
}
},
{
name: 'n8n_manage_datatable',
description: `Manage n8n data tables and rows. Actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows. Requires n8n enterprise/cloud with data tables feature.`,
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['createTable', 'listTables', 'getTable', 'updateTable', 'deleteTable', 'getRows', 'insertRows', 'updateRows', 'upsertRows', 'deleteRows'],
description: 'Operation to perform',
},
tableId: { type: 'string', description: 'Data table ID (required for all actions except createTable and listTables)' },
name: { type: 'string', description: 'For createTable/updateTable: table name' },
columns: {
type: 'array',
description: 'For createTable: column definitions',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] },
},
required: ['name'],
},
},
data: { description: 'For insertRows: array of row objects. For updateRows/upsertRows: object with column values.' },
filter: {
type: 'object',
description: 'For getRows/updateRows/upsertRows/deleteRows: {type?: "and"|"or", filters: [{columnName, condition, value}]}',
},
limit: { type: 'number', description: 'For listTables/getRows: max results (1-100)' },
cursor: { type: 'string', description: 'For listTables/getRows: pagination cursor' },
sortBy: { type: 'string', description: 'For getRows: "columnName:asc" or "columnName:desc"' },
search: { type: 'string', description: 'For getRows: text search across string columns' },
returnType: { type: 'string', enum: ['count', 'id', 'all'], description: 'For insertRows: what to return (default: count)' },
returnData: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: return affected rows (default: false)' },
dryRun: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: preview without applying (default: false)' },
},
required: ['action'],
},
annotations: {
title: 'Manage Data Tables',
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
},
];
//# sourceMappingURL=tools-n8n-manager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import { Workflow, WorkflowListParams, WorkflowListResponse, Execution, ExecutionListParams, ExecutionListResponse, Credential, CredentialListParams, CredentialListResponse, Tag, TagListParams, TagListResponse, HealthCheckResponse, N8nVersionInfo, Variable, WebhookRequest, SourceControlStatus, SourceControlPullResult, SourceControlPushResult } from '../types/n8n-api';
import { Workflow, WorkflowListParams, WorkflowListResponse, Execution, ExecutionListParams, ExecutionListResponse, Credential, CredentialListParams, CredentialListResponse, Tag, TagListParams, TagListResponse, HealthCheckResponse, N8nVersionInfo, Variable, WebhookRequest, SourceControlStatus, SourceControlPullResult, SourceControlPushResult, DataTable, DataTableColumn, DataTableListParams, DataTableRow, DataTableRowListParams, DataTableInsertRowsParams, DataTableUpdateRowsParams, DataTableUpsertRowParams, DataTableDeleteRowsParams } from '../types/n8n-api';
export interface N8nApiClientConfig {
baseUrl: string;
apiKey: string;
@@ -45,6 +45,28 @@ export declare class N8nApiClient {
createVariable(variable: Partial<Variable>): Promise<Variable>;
updateVariable(id: string, variable: Partial<Variable>): Promise<Variable>;
deleteVariable(id: string): Promise<void>;
createDataTable(params: {
name: string;
columns?: DataTableColumn[];
}): Promise<DataTable>;
listDataTables(params?: DataTableListParams): Promise<{
data: DataTable[];
nextCursor?: string | null;
}>;
getDataTable(id: string): Promise<DataTable>;
updateDataTable(id: string, params: {
name: string;
}): Promise<DataTable>;
deleteDataTable(id: string): Promise<void>;
getDataTableRows(id: string, params?: DataTableRowListParams): Promise<{
data: DataTableRow[];
nextCursor?: string | null;
}>;
insertDataTableRows(id: string, params: DataTableInsertRowsParams): Promise<any>;
updateDataTableRows(id: string, params: DataTableUpdateRowsParams): Promise<any>;
upsertDataTableRow(id: string, params: DataTableUpsertRowParams): Promise<any>;
deleteDataTableRows(id: string, params: DataTableDeleteRowsParams): Promise<any>;
private serializeDataTableParams;
private validateListResponse;
}
//# sourceMappingURL=n8n-api-client.d.ts.map

View File

@@ -1 +1 @@
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,GAAG,EACH,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,QAAQ,EACR,cAAc,EAGd,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,kBAAkB,CAAC;AAS1B,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,cAAc,CAA+C;gBAEzD,MAAM,EAAE,kBAAkB;IAqDhC,UAAU,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;YAyBpC,gBAAgB;IAa9B,oBAAoB,IAAI,cAAc,GAAG,IAAI;IAKvC,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IA6C3C,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAU9D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsC1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS7C,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS/C,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsBjD,aAAa,CAAC,MAAM,GAAE,kBAAuB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAU7E,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAwBjE,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAShF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1C,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC;IAiErD,eAAe,CAAC,MAAM,GAAE,oBAAyB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IASnF,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAS9C,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAStE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IASlF,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB3C,QAAQ,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;IAS9D,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAS1C,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAStD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAUxE,sBAAsB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAStD,iBAAiB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,uBAAuB,CAAC;IASlE,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,uBAAuB,CAAC;IAa7B,YAAY,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAWnC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS9D,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzC,eAAe,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAS1F,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAS5G,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAS5C,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IASzE,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1C,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAYhI,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,GAAG,CAAC;IAShF,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,GAAG,CAAC;IAShF,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,GAAG,CAAC;IAS9E,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,GAAG,CAAC;IAgBtF,OAAO,CAAC,wBAAwB;IAkBhC,OAAO,CAAC,oBAAoB;CAmC7B"}

View File

@@ -448,6 +448,110 @@ class N8nApiClient {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async createDataTable(params) {
try {
const response = await this.client.post('/data-tables', params);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async listDataTables(params = {}) {
try {
const response = await this.client.get('/data-tables', { params });
return this.validateListResponse(response.data, 'data-tables');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getDataTable(id) {
try {
const response = await this.client.get(`/data-tables/${id}`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateDataTable(id, params) {
try {
const response = await this.client.patch(`/data-tables/${id}`, params);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteDataTable(id) {
try {
await this.client.delete(`/data-tables/${id}`);
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getDataTableRows(id, params = {}) {
try {
const response = await this.client.get(`/data-tables/${id}/rows`, {
params,
paramsSerializer: (p) => this.serializeDataTableParams(p),
});
return this.validateListResponse(response.data, 'data-table-rows');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async insertDataTableRows(id, params) {
try {
const response = await this.client.post(`/data-tables/${id}/rows`, params);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateDataTableRows(id, params) {
try {
const response = await this.client.patch(`/data-tables/${id}/rows/update`, params);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async upsertDataTableRow(id, params) {
try {
const response = await this.client.post(`/data-tables/${id}/rows/upsert`, params);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteDataTableRows(id, params) {
try {
const response = await this.client.delete(`/data-tables/${id}/rows/delete`, {
params,
paramsSerializer: (p) => this.serializeDataTableParams(p),
});
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
serializeDataTableParams(params) {
const parts = [];
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null)
continue;
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
}
return parts.join('&');
}
validateListResponse(responseData, resourceType) {
if (!responseData || typeof responseData !== 'object') {
throw new Error(`Invalid response from n8n API for ${resourceType}: response is not an object`);

File diff suppressed because one or more lines are too long

View File

@@ -374,4 +374,69 @@ export interface ErrorSuggestion {
description: string;
confidence: 'high' | 'medium' | 'low';
}
export interface DataTableColumn {
name: string;
type?: 'string' | 'number' | 'boolean' | 'date';
}
export interface DataTableColumnResponse {
id: string;
name: string;
type: 'string' | 'number' | 'boolean' | 'date';
index: number;
}
export interface DataTable {
id: string;
name: string;
columns?: DataTableColumnResponse[];
projectId?: string;
createdAt?: string;
updatedAt?: string;
}
export interface DataTableRow {
id?: number;
createdAt?: string;
updatedAt?: string;
[columnName: string]: unknown;
}
export interface DataTableFilterCondition {
columnName: string;
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
value?: any;
}
export interface DataTableFilter {
type?: 'and' | 'or';
filters: DataTableFilterCondition[];
}
export interface DataTableListParams {
limit?: number;
cursor?: string;
}
export interface DataTableRowListParams {
limit?: number;
cursor?: string;
filter?: string;
sortBy?: string;
search?: string;
}
export interface DataTableInsertRowsParams {
data: Record<string, unknown>[];
returnType?: 'count' | 'id' | 'all';
}
export interface DataTableUpdateRowsParams {
filter: DataTableFilter;
data: Record<string, unknown>;
returnData?: boolean;
dryRun?: boolean;
}
export interface DataTableUpsertRowParams {
filter: DataTableFilter;
data: Record<string, unknown>;
returnData?: boolean;
dryRun?: boolean;
}
export interface DataTableDeleteRowsParams {
filter: string;
returnData?: boolean;
dryRun?: boolean;
}
//# sourceMappingURL=n8n-api.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ export declare class N8nAuthenticationError extends N8nApiError {
constructor(message?: string);
}
export declare class N8nNotFoundError extends N8nApiError {
constructor(resource: string, id?: string);
constructor(messageOrResource: string, id?: string);
}
export declare class N8nValidationError extends N8nApiError {
constructor(message: string, details?: unknown);

View File

@@ -1 +1 @@
{"version":3,"file":"n8n-errors.d.ts","sourceRoot":"","sources":["../../src/utils/n8n-errors.ts"],"names":[],"mappings":"AAIA,qBAAa,WAAY,SAAQ,KAAK;IAG3B,UAAU,CAAC,EAAE,MAAM;IACnB,IAAI,CAAC,EAAE,MAAM;IACb,OAAO,CAAC,EAAE,OAAO;gBAHxB,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,OAAO,CAAC,EAAE,OAAO,YAAA;CAK3B;AAED,qBAAa,sBAAuB,SAAQ,WAAW;gBACzC,OAAO,SAA0B;CAI9C;AAED,qBAAa,gBAAiB,SAAQ,WAAW;gBACnC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM;CAK1C;AAED,qBAAa,kBAAmB,SAAQ,WAAW;gBACrC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAI/C;AAED,qBAAa,iBAAkB,SAAQ,WAAW;gBACpC,UAAU,CAAC,EAAE,MAAM;CAOhC;AAED,qBAAa,cAAe,SAAQ,WAAW;gBACjC,OAAO,SAA0B,EAAE,UAAU,SAAM;CAIhE;AAGD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAuC7D;AAQD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrF;AAMD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAGD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAmBtE;AAGD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBtE"}
{"version":3,"file":"n8n-errors.d.ts","sourceRoot":"","sources":["../../src/utils/n8n-errors.ts"],"names":[],"mappings":"AAIA,qBAAa,WAAY,SAAQ,KAAK;IAG3B,UAAU,CAAC,EAAE,MAAM;IACnB,IAAI,CAAC,EAAE,MAAM;IACb,OAAO,CAAC,EAAE,OAAO;gBAHxB,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,OAAO,CAAC,EAAE,OAAO,YAAA;CAK3B;AAED,qBAAa,sBAAuB,SAAQ,WAAW;gBACzC,OAAO,SAA0B;CAI9C;AAED,qBAAa,gBAAiB,SAAQ,WAAW;gBACnC,iBAAiB,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM;CAOnD;AAED,qBAAa,kBAAmB,SAAQ,WAAW;gBACrC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAI/C;AAED,qBAAa,iBAAkB,SAAQ,WAAW;gBACpC,UAAU,CAAC,EAAE,MAAM;CAOhC;AAED,qBAAa,cAAe,SAAQ,WAAW;gBACjC,OAAO,SAA0B,EAAE,UAAU,SAAM;CAIhE;AAGD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAuC7D;AAQD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrF;AAMD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAGD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAmBtE;AAGD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBtE"}

View File

@@ -25,8 +25,8 @@ class N8nAuthenticationError extends N8nApiError {
}
exports.N8nAuthenticationError = N8nAuthenticationError;
class N8nNotFoundError extends N8nApiError {
constructor(resource, id) {
const message = id ? `${resource} with ID ${id} not found` : `${resource} not found`;
constructor(messageOrResource, id) {
const message = id ? `${messageOrResource} with ID ${id} not found` : messageOrResource;
super(message, 404, 'NOT_FOUND');
this.name = 'N8nNotFoundError';
}
@@ -69,7 +69,7 @@ function handleN8nApiError(error) {
case 401:
return new N8nAuthenticationError(message);
case 404:
return new N8nNotFoundError('Resource', message);
return new N8nNotFoundError(message || 'Resource');
case 400:
return new N8nValidationError(message, data);
case 429:

View File

@@ -1 +1 @@
{"version":3,"file":"n8n-errors.js","sourceRoot":"","sources":["../../src/utils/n8n-errors.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"n8n-errors.js","sourceRoot":"","sources":["../../src/utils/n8n-errors.ts"],"names":[],"mappings":"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"}

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.38.0",
"version": "2.40.3",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -253,6 +253,21 @@ export class SingleSessionHTTPServer {
// This ensures compatibility with all MCP clients and proxies
return Boolean(sessionId && sessionId.length > 0);
}
/**
* Checks if a request body is a JSON-RPC notification (or batch of only notifications).
* Notifications have a `method` field but no `id` field per JSON-RPC 2.0 spec.
*/
private isJsonRpcNotification(body: unknown): boolean {
if (!body || typeof body !== 'object') return false;
if (Array.isArray(body)) {
return body.length > 0 && body.every(
(msg: any) => msg && typeof msg.method === 'string' && !('id' in msg)
);
}
const msg = body as Record<string, unknown>;
return typeof msg.method === 'string' && !('id' in msg);
}
/**
* Sanitize error information for client responses
@@ -614,6 +629,24 @@ export class SingleSessionHTTPServer {
logger.info('handleRequest: Reusing existing transport for session', { sessionId });
transport = this.transports[sessionId];
// TOCTOU guard: session may have been removed between the check and this line
if (!transport) {
if (this.isJsonRpcNotification(req.body)) {
logger.info('handleRequest: Session removed during lookup, accepting notification', {
sessionId,
});
res.status(202).end();
return;
}
logger.warn('handleRequest: Session removed between check and use (TOCTOU)', { sessionId });
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Bad Request: Session not found or expired' },
id: req.body?.id || null,
});
return;
}
// In multi-tenant shared mode, update instance context if provided
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
const sessionStrategy = process.env.MULTI_TENANT_SESSION_STRATEGY || 'instance';
@@ -627,23 +660,36 @@ export class SingleSessionHTTPServer {
this.updateSessionAccess(sessionId);
} else {
// Invalid request - no session ID and not an initialize request
// Check if this is a JSON-RPC notification (no "id" field = fire-and-forget)
// Per JSON-RPC 2.0 spec, notifications don't expect responses.
// Returning 400 for stale-session notifications causes Claude's proxy to
// interpret the connection as broken, triggering reconnection storms (#654).
if (this.isJsonRpcNotification(req.body)) {
logger.info('handleRequest: Accepting notification for stale/missing session', {
method: req.body?.method,
sessionId: sessionId || 'none',
});
res.status(202).end();
return;
}
// Only return 400 for actual requests that need a valid session
const errorDetails = {
hasSessionId: !!sessionId,
isInitialize: isInitialize,
sessionIdValid: sessionId ? this.isValidSessionId(sessionId) : false,
sessionExists: sessionId ? !!this.transports[sessionId] : false
};
logger.warn('handleRequest: Invalid request - no session ID and not initialize', errorDetails);
let errorMessage = 'Bad Request: No valid session ID provided and not an initialize request';
if (sessionId && !this.isValidSessionId(sessionId)) {
errorMessage = 'Bad Request: Invalid session ID format';
} else if (sessionId && !this.transports[sessionId]) {
errorMessage = 'Bad Request: Session not found or expired';
}
res.status(400).json({
jsonrpc: '2.0',
error: {

View File

@@ -8,7 +8,7 @@ import {
WebhookRequest,
McpToolResponse,
ExecutionFilterOptions,
ExecutionMode
ExecutionMode,
} from '../types/n8n-api';
import type { TriggerType, TestWorkflowInput } from '../triggers/types';
import {
@@ -383,6 +383,7 @@ const createWorkflowSchema = z.object({
executionTimeout: z.number().optional(),
errorWorkflow: z.string().optional(),
}).optional(),
projectId: z.string().optional(),
});
const updateWorkflowSchema = z.object({
@@ -1974,7 +1975,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
// Check which tools are available
const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
const managementTools = apiConfigured ? 13 : 0; // Management tools requiring API (includes n8n_deploy_template)
const managementTools = apiConfigured ? 14 : 0; // Management tools requiring API (includes n8n_manage_datatable)
const totalTools = documentationTools + managementTools;
// Check npm version
@@ -2688,3 +2689,260 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst
};
}
}
// ========================================================================
// Data Table Handlers
// ========================================================================
// Shared Zod schemas for data table operations
const dataTableFilterConditionSchema = z.object({
columnName: z.string().min(1),
condition: z.enum(['eq', 'neq', 'like', 'ilike', 'gt', 'gte', 'lt', 'lte']),
value: z.any(),
});
const dataTableFilterSchema = z.object({
type: z.enum(['and', 'or']).optional().default('and'),
filters: z.array(dataTableFilterConditionSchema).min(1, 'At least one filter condition is required'),
});
// Shared base schema for actions requiring a tableId
const tableIdSchema = z.object({
tableId: z.string().min(1, 'tableId is required'),
});
// Per-action Zod schemas
const createTableSchema = z.object({
name: z.string().min(1, 'Table name cannot be empty'),
columns: z.array(z.object({
name: z.string().min(1, 'Column name cannot be empty'),
type: z.enum(['string', 'number', 'boolean', 'date']).optional(),
})).optional(),
});
const listTablesSchema = z.object({
limit: z.number().min(1).max(100).optional(),
cursor: z.string().optional(),
});
const updateTableSchema = tableIdSchema.extend({
name: z.string().min(1, 'New table name cannot be empty'),
});
// MCP transports may serialize JSON objects/arrays as strings.
// Parse them back, but return the original value on failure so Zod reports a proper type error.
function tryParseJson(val: unknown): unknown {
if (typeof val !== 'string') return val;
try { return JSON.parse(val); } catch { return val; }
}
const coerceJsonArray = z.preprocess(tryParseJson, z.array(z.record(z.unknown())));
const coerceJsonObject = z.preprocess(tryParseJson, z.record(z.unknown()));
const coerceJsonFilter = z.preprocess(tryParseJson, dataTableFilterSchema);
const getRowsSchema = tableIdSchema.extend({
limit: z.number().min(1).max(100).optional(),
cursor: z.string().optional(),
filter: z.union([coerceJsonFilter, z.string()]).optional(),
sortBy: z.string().optional(),
search: z.string().optional(),
});
const insertRowsSchema = tableIdSchema.extend({
data: coerceJsonArray.pipe(z.array(z.record(z.unknown())).min(1, 'At least one row is required')),
returnType: z.enum(['count', 'id', 'all']).optional(),
});
// Shared schema for update/upsert (identical structure)
const mutateRowsSchema = tableIdSchema.extend({
filter: coerceJsonFilter,
data: coerceJsonObject,
returnData: z.boolean().optional(),
dryRun: z.boolean().optional(),
});
const deleteRowsSchema = tableIdSchema.extend({
filter: coerceJsonFilter,
returnData: z.boolean().optional(),
dryRun: z.boolean().optional(),
});
function handleDataTableError(error: unknown): McpToolResponse {
if (error instanceof z.ZodError) {
return { success: false, error: 'Invalid input', details: { errors: error.errors } };
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code,
details: error.details as Record<string, unknown> | undefined,
};
}
return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' };
}
export async function handleCreateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const input = createTableSchema.parse(args);
const dataTable = await client.createDataTable(input);
if (!dataTable || !dataTable.id) {
return { success: false, error: 'Data table creation failed: n8n API returned an empty or invalid response' };
}
return {
success: true,
data: { id: dataTable.id, name: dataTable.name },
message: `Data table "${dataTable.name}" created with ID: ${dataTable.id}`,
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleListTables(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const input = listTablesSchema.parse(args || {});
const result = await client.listDataTables(input);
return {
success: true,
data: {
tables: result.data,
count: result.data.length,
nextCursor: result.nextCursor || undefined,
},
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleGetTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId } = tableIdSchema.parse(args);
const dataTable = await client.getDataTable(tableId);
return { success: true, data: dataTable };
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleUpdateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, name } = updateTableSchema.parse(args);
const dataTable = await client.updateDataTable(tableId, { name });
const rawArgs = args as Record<string, unknown>;
const hasColumns = rawArgs && typeof rawArgs === 'object' && 'columns' in rawArgs;
return {
success: true,
data: dataTable,
message: `Data table renamed to "${dataTable.name}"` +
(hasColumns ? '. Note: columns parameter was ignored — table schema is immutable after creation via the public API' : ''),
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleDeleteTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId } = tableIdSchema.parse(args);
await client.deleteDataTable(tableId);
return { success: true, message: `Data table ${tableId} deleted successfully` };
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleGetRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, filter, sortBy, ...params } = getRowsSchema.parse(args);
const queryParams: Record<string, unknown> = { ...params };
if (filter) {
queryParams.filter = typeof filter === 'string' ? filter : JSON.stringify(filter);
}
if (sortBy) {
queryParams.sortBy = sortBy;
}
const result = await client.getDataTableRows(tableId, queryParams as any);
return {
success: true,
data: {
rows: result.data,
count: result.data.length,
nextCursor: result.nextCursor || undefined,
},
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleInsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = insertRowsSchema.parse(args);
const result = await client.insertDataTableRows(tableId, params);
return {
success: true,
data: result,
message: `Rows inserted into data table ${tableId}`,
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleUpdateRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = mutateRowsSchema.parse(args);
const result = await client.updateDataTableRows(tableId, params);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: rows matched (no changes applied)' : 'Rows updated successfully',
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleUpsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, ...params } = mutateRowsSchema.parse(args);
const result = await client.upsertDataTableRow(tableId, params);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: upsert previewed (no changes applied)' : 'Row upserted successfully',
};
} catch (error) {
return handleDataTableError(error);
}
}
export async function handleDeleteRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured(context);
const { tableId, filter, ...params } = deleteRowsSchema.parse(args);
const queryParams = {
filter: JSON.stringify(filter),
...params,
};
const result = await client.deleteDataTableRows(tableId, queryParams as any);
return {
success: true,
data: result,
message: params.dryRun ? 'Dry run: rows matched for deletion (no changes applied)' : 'Rows deleted successfully',
};
} catch (error) {
return handleDataTableError(error);
}
}

View File

@@ -1029,6 +1029,11 @@ export class N8NDocumentationMCPServer {
? { valid: true, errors: [] }
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
break;
case 'n8n_manage_datatable':
validationResult = args.action
? { valid: true, errors: [] }
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
break;
case 'n8n_deploy_template':
// Requires templateId parameter
validationResult = args.templateId !== undefined
@@ -1496,6 +1501,26 @@ export class N8NDocumentationMCPServer {
if (!this.repository) throw new Error('Repository not initialized');
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
case 'n8n_manage_datatable': {
this.validateToolParams(name, args, ['action']);
const dtAction = args.action;
// Each handler validates its own inputs via Zod schemas
switch (dtAction) {
case 'createTable': return n8nHandlers.handleCreateTable(args, this.instanceContext);
case 'listTables': return n8nHandlers.handleListTables(args, this.instanceContext);
case 'getTable': return n8nHandlers.handleGetTable(args, this.instanceContext);
case 'updateTable': return n8nHandlers.handleUpdateTable(args, this.instanceContext);
case 'deleteTable': return n8nHandlers.handleDeleteTable(args, this.instanceContext);
case 'getRows': return n8nHandlers.handleGetRows(args, this.instanceContext);
case 'insertRows': return n8nHandlers.handleInsertRows(args, this.instanceContext);
case 'updateRows': return n8nHandlers.handleUpdateRows(args, this.instanceContext);
case 'upsertRows': return n8nHandlers.handleUpsertRows(args, this.instanceContext);
case 'deleteRows': return n8nHandlers.handleDeleteRows(args, this.instanceContext);
default:
throw new Error(`Unknown action: ${dtAction}. Valid actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows`);
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}

View File

@@ -22,7 +22,8 @@ import {
n8nTestWorkflowDoc,
n8nExecutionsDoc,
n8nWorkflowVersionsDoc,
n8nDeployTemplateDoc
n8nDeployTemplateDoc,
n8nManageDatatableDoc
} from './workflow_management';
// Combine all tool documentations into a single object
@@ -60,7 +61,8 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
n8n_test_workflow: n8nTestWorkflowDoc,
n8n_executions: n8nExecutionsDoc,
n8n_workflow_versions: n8nWorkflowVersionsDoc,
n8n_deploy_template: n8nDeployTemplateDoc
n8n_deploy_template: n8nDeployTemplateDoc,
n8n_manage_datatable: n8nManageDatatableDoc
};
// Re-export types

View File

@@ -10,3 +10,4 @@ export { n8nTestWorkflowDoc } from './n8n-test-workflow';
export { n8nExecutionsDoc } from './n8n-executions';
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
export { n8nManageDatatableDoc } from './n8n-manage-datatable';

View File

@@ -0,0 +1,109 @@
import { ToolDocumentation } from '../types';
export const n8nManageDatatableDoc: ToolDocumentation = {
name: 'n8n_manage_datatable',
category: 'workflow_management',
essentials: {
description: 'Manage n8n data tables and rows. Unified tool for table CRUD and row operations with filtering, pagination, and dry-run support.',
keyParameters: ['action', 'tableId', 'name', 'data', 'filter'],
example: 'n8n_manage_datatable({action: "createTable", name: "Contacts", columns: [{name: "email", type: "string"}]})',
performance: 'Fast (100-500ms)',
tips: [
'Table actions: createTable, listTables, getTable, updateTable (rename only), deleteTable',
'Row actions: getRows, insertRows, updateRows, upsertRows, deleteRows',
'Use dryRun: true to preview update/upsert/delete before applying',
'Filter supports: eq, neq, like, ilike, gt, gte, lt, lte conditions',
'Use returnData: true to get affected rows back from update/upsert/delete',
'Requires n8n enterprise or cloud with data tables feature'
]
},
full: {
description: `**Table Actions:**
- **createTable**: Create a new data table with optional typed columns
- **listTables**: List all data tables (paginated)
- **getTable**: Get table details and column definitions by ID
- **updateTable**: Rename an existing table (name only — column modifications not supported via API)
- **deleteTable**: Permanently delete a table and all its rows
**Row Actions:**
- **getRows**: List rows with filtering, sorting, search, and pagination
- **insertRows**: Insert one or more rows (bulk)
- **updateRows**: Update rows matching a filter condition
- **upsertRows**: Update matching row or insert if none match
- **deleteRows**: Delete rows matching a filter condition (filter required)
**Filter System:** Used in getRows, updateRows, upsertRows, deleteRows
- Combine conditions with "and" (default) or "or"
- Conditions: eq, neq, like, ilike, gt, gte, lt, lte
- Example: {type: "and", filters: [{columnName: "status", condition: "eq", value: "active"}]}
**Dry Run:** updateRows, upsertRows, and deleteRows support dryRun: true to preview changes without applying them.`,
parameters: {
action: { type: 'string', required: true, description: 'Operation to perform' },
tableId: { type: 'string', required: false, description: 'Data table ID (required for all except createTable and listTables)' },
name: { type: 'string', required: false, description: 'For createTable/updateTable: table name' },
columns: { type: 'array', required: false, description: 'For createTable: column definitions [{name, type?}]. Types: string, number, boolean, date' },
data: { type: 'array|object', required: false, description: 'For insertRows: array of row objects. For updateRows/upsertRows: object with column values' },
filter: { type: 'object', required: false, description: 'Filter: {type?: "and"|"or", filters: [{columnName, condition, value}]}' },
limit: { type: 'number', required: false, description: 'For listTables/getRows: max results (1-100)' },
cursor: { type: 'string', required: false, description: 'For listTables/getRows: pagination cursor' },
sortBy: { type: 'string', required: false, description: 'For getRows: "columnName:asc" or "columnName:desc"' },
search: { type: 'string', required: false, description: 'For getRows: full-text search across string columns' },
returnType: { type: 'string', required: false, description: 'For insertRows: "count" (default), "id", or "all"' },
returnData: { type: 'boolean', required: false, description: 'For updateRows/upsertRows/deleteRows: return affected rows (default: false)' },
dryRun: { type: 'boolean', required: false, description: 'For updateRows/upsertRows/deleteRows: preview without applying (default: false)' },
},
returns: `Depends on action:
- createTable: {id, name}
- listTables: {tables, count, nextCursor?}
- getTable: Full table object with columns
- updateTable: Updated table object
- deleteTable: Success message
- getRows: {rows, count, nextCursor?}
- insertRows: Depends on returnType (count/ids/rows)
- updateRows: Update result with optional rows
- upsertRows: Upsert result with action type
- deleteRows: Delete result with optional rows`,
examples: [
'// Create a table\nn8n_manage_datatable({action: "createTable", name: "Contacts", columns: [{name: "email", type: "string"}, {name: "score", type: "number"}]})',
'// List all tables\nn8n_manage_datatable({action: "listTables"})',
'// Get table details\nn8n_manage_datatable({action: "getTable", tableId: "dt-123"})',
'// Rename a table\nn8n_manage_datatable({action: "updateTable", tableId: "dt-123", name: "New Name"})',
'// Delete a table\nn8n_manage_datatable({action: "deleteTable", tableId: "dt-123"})',
'// Get rows with filter\nn8n_manage_datatable({action: "getRows", tableId: "dt-123", filter: {filters: [{columnName: "status", condition: "eq", value: "active"}]}, limit: 50})',
'// Search rows\nn8n_manage_datatable({action: "getRows", tableId: "dt-123", search: "john", sortBy: "name:asc"})',
'// Insert rows\nn8n_manage_datatable({action: "insertRows", tableId: "dt-123", data: [{email: "a@b.com", score: 10}], returnType: "all"})',
'// Update rows (dry run)\nn8n_manage_datatable({action: "updateRows", tableId: "dt-123", filter: {filters: [{columnName: "score", condition: "lt", value: 5}]}, data: {status: "inactive"}, dryRun: true})',
'// Upsert a row\nn8n_manage_datatable({action: "upsertRows", tableId: "dt-123", filter: {filters: [{columnName: "email", condition: "eq", value: "a@b.com"}]}, data: {score: 15}, returnData: true})',
'// Delete rows\nn8n_manage_datatable({action: "deleteRows", tableId: "dt-123", filter: {filters: [{columnName: "status", condition: "eq", value: "deleted"}]}})',
],
useCases: [
'Persist structured workflow data across executions',
'Store and query lookup tables for workflow logic',
'Bulk insert records from external data sources',
'Conditionally update records matching criteria',
'Upsert to maintain unique records by key column',
'Clean up old or invalid rows with filtered delete',
'Preview changes with dryRun before modifying data',
],
performance: 'Table operations: 50-300ms. Row operations: 100-500ms depending on data size and filters.',
bestPractices: [
'Define column types upfront for schema consistency',
'Use dryRun: true before bulk updates/deletes to verify filter correctness',
'Use returnType: "count" (default) for insertRows to minimize response size',
'Use filter with specific conditions to avoid unintended bulk operations',
'Use cursor-based pagination for large result sets',
'Use sortBy for deterministic row ordering',
],
pitfalls: [
'Requires N8N_API_URL and N8N_API_KEY configured',
'Feature only available on n8n enterprise or cloud plans',
'deleteTable permanently deletes all rows — cannot be undone',
'deleteRows requires a filter — cannot delete all rows without one',
'Column types cannot be changed after table creation via API',
'updateTable can only rename the table (no column modifications via public API)',
'projectId cannot be set via the public API — use the n8n UI',
],
relatedTools: ['n8n_create_workflow', 'n8n_list_workflows', 'n8n_health_check'],
},
};

View File

@@ -63,6 +63,10 @@ export const n8nManagementTools: ToolDefinition[] = [
executionTimeout: { type: 'number' },
errorWorkflow: { type: 'string' }
}
},
projectId: {
type: 'string',
description: 'Optional project ID to create the workflow in (enterprise feature)'
}
},
required: ['name', 'nodes', 'connections']
@@ -602,5 +606,52 @@ export const n8nManagementTools: ToolDefinition[] = [
destructiveHint: false,
openWorldHint: true,
},
}
},
{
name: 'n8n_manage_datatable',
description: `Manage n8n data tables and rows. Actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows. Requires n8n enterprise/cloud with data tables feature.`,
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['createTable', 'listTables', 'getTable', 'updateTable', 'deleteTable', 'getRows', 'insertRows', 'updateRows', 'upsertRows', 'deleteRows'],
description: 'Operation to perform',
},
tableId: { type: 'string', description: 'Data table ID (required for all actions except createTable and listTables)' },
name: { type: 'string', description: 'For createTable: table name. For updateTable: new name (rename only — schema is immutable after creation)' },
columns: {
type: 'array',
description: 'For createTable only: column definitions (schema is immutable after creation via public API)',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] },
},
required: ['name'],
},
},
data: { description: 'For insertRows: array of row objects. For updateRows/upsertRows: object with column values.' },
filter: {
type: 'object',
description: 'For getRows/updateRows/upsertRows/deleteRows: {type?: "and"|"or", filters: [{columnName, condition, value}]}',
},
limit: { type: 'number', description: 'For listTables/getRows: max results (1-100)' },
cursor: { type: 'string', description: 'For listTables/getRows: pagination cursor' },
sortBy: { type: 'string', description: 'For getRows: "columnName:asc" or "columnName:desc"' },
search: { type: 'string', description: 'For getRows: text search across string columns' },
returnType: { type: 'string', enum: ['count', 'id', 'all'], description: 'For insertRows: what to return (default: count)' },
returnData: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: return affected rows (default: false)' },
dryRun: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: preview without applying (default: false)' },
},
required: ['action'],
},
annotations: {
title: 'Manage Data Tables',
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
},
];

View File

@@ -22,6 +22,15 @@ import {
SourceControlStatus,
SourceControlPullResult,
SourceControlPushResult,
DataTable,
DataTableColumn,
DataTableListParams,
DataTableRow,
DataTableRowListParams,
DataTableInsertRowsParams,
DataTableUpdateRowsParams,
DataTableUpsertRowParams,
DataTableDeleteRowsParams,
} from '../types/n8n-api';
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
@@ -582,6 +591,114 @@ export class N8nApiClient {
}
}
async createDataTable(params: { name: string; columns?: DataTableColumn[] }): Promise<DataTable> {
try {
const response = await this.client.post('/data-tables', params);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async listDataTables(params: DataTableListParams = {}): Promise<{ data: DataTable[]; nextCursor?: string | null }> {
try {
const response = await this.client.get('/data-tables', { params });
return this.validateListResponse<DataTable>(response.data, 'data-tables');
} catch (error) {
throw handleN8nApiError(error);
}
}
async getDataTable(id: string): Promise<DataTable> {
try {
const response = await this.client.get(`/data-tables/${id}`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateDataTable(id: string, params: { name: string }): Promise<DataTable> {
try {
const response = await this.client.patch(`/data-tables/${id}`, params);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteDataTable(id: string): Promise<void> {
try {
await this.client.delete(`/data-tables/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
async getDataTableRows(id: string, params: DataTableRowListParams = {}): Promise<{ data: DataTableRow[]; nextCursor?: string | null }> {
try {
const response = await this.client.get(`/data-tables/${id}/rows`, {
params,
paramsSerializer: (p) => this.serializeDataTableParams(p),
});
return this.validateListResponse<DataTableRow>(response.data, 'data-table-rows');
} catch (error) {
throw handleN8nApiError(error);
}
}
async insertDataTableRows(id: string, params: DataTableInsertRowsParams): Promise<any> {
try {
const response = await this.client.post(`/data-tables/${id}/rows`, params);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateDataTableRows(id: string, params: DataTableUpdateRowsParams): Promise<any> {
try {
const response = await this.client.patch(`/data-tables/${id}/rows/update`, params);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async upsertDataTableRow(id: string, params: DataTableUpsertRowParams): Promise<any> {
try {
const response = await this.client.post(`/data-tables/${id}/rows/upsert`, params);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteDataTableRows(id: string, params: DataTableDeleteRowsParams): Promise<any> {
try {
const response = await this.client.delete(`/data-tables/${id}/rows/delete`, {
params,
paramsSerializer: (p) => this.serializeDataTableParams(p),
});
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
/**
* Serializes data table query params with explicit encodeURIComponent.
* Axios's default serializer doesn't encode some reserved chars that n8n rejects.
*/
private serializeDataTableParams(params: Record<string, any>): string {
const parts: string[] = [];
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
}
return parts.join('&');
}
/**
* Validates and normalizes n8n API list responses.
* Handles both modern format {data: [], nextCursor?: string} and legacy array format.

View File

@@ -454,4 +454,82 @@ export interface ErrorSuggestion {
title: string;
description: string;
confidence: 'high' | 'medium' | 'low';
}
// Data Table types
export interface DataTableColumn {
name: string;
type?: 'string' | 'number' | 'boolean' | 'date';
}
export interface DataTableColumnResponse {
id: string;
name: string;
type: 'string' | 'number' | 'boolean' | 'date';
index: number;
}
export interface DataTable {
id: string;
name: string;
columns?: DataTableColumnResponse[];
projectId?: string;
createdAt?: string;
updatedAt?: string;
}
export interface DataTableRow {
id?: number;
createdAt?: string;
updatedAt?: string;
[columnName: string]: unknown;
}
export interface DataTableFilterCondition {
columnName: string;
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
value?: any;
}
export interface DataTableFilter {
type?: 'and' | 'or';
filters: DataTableFilterCondition[];
}
export interface DataTableListParams {
limit?: number;
cursor?: string;
}
export interface DataTableRowListParams {
limit?: number;
cursor?: string;
filter?: string;
sortBy?: string;
search?: string;
}
export interface DataTableInsertRowsParams {
data: Record<string, unknown>[];
returnType?: 'count' | 'id' | 'all';
}
export interface DataTableUpdateRowsParams {
filter: DataTableFilter;
data: Record<string, unknown>;
returnData?: boolean;
dryRun?: boolean;
}
export interface DataTableUpsertRowParams {
filter: DataTableFilter;
data: Record<string, unknown>;
returnData?: boolean;
dryRun?: boolean;
}
export interface DataTableDeleteRowsParams {
filter: string;
returnData?: boolean;
dryRun?: boolean;
}

View File

@@ -22,8 +22,10 @@ export class N8nAuthenticationError extends N8nApiError {
}
export class N8nNotFoundError extends N8nApiError {
constructor(resource: string, id?: string) {
const message = id ? `${resource} with ID ${id} not found` : `${resource} not found`;
constructor(messageOrResource: string, id?: string) {
// If id is provided, format as "resource with ID id not found"
// Otherwise, use messageOrResource as-is (it's already a complete message from the API)
const message = id ? `${messageOrResource} with ID ${id} not found` : messageOrResource;
super(message, 404, 'NOT_FOUND');
this.name = 'N8nNotFoundError';
}
@@ -70,7 +72,7 @@ export function handleN8nApiError(error: unknown): N8nApiError {
case 401:
return new N8nAuthenticationError(message);
case 404:
return new N8nNotFoundError('Resource', message);
return new N8nNotFoundError(message || 'Resource');
case 400:
return new N8nValidationError(message, data);
case 429:

View File

@@ -219,6 +219,7 @@ describe('HTTP Server Session Management', () => {
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
end: vi.fn().mockReturnThis(),
setHeader: vi.fn((key: string, value: string) => {
headers[key.toLowerCase()] = value;
}),
@@ -1170,7 +1171,7 @@ describe('HTTP Server Session Management', () => {
it('should show legacy SSE session when present', async () => {
server = new SingleSessionHTTPServer();
// Mock legacy session
const mockSession = {
sessionId: 'sse-session-123',
@@ -1180,10 +1181,111 @@ describe('HTTP Server Session Management', () => {
(server as any).session = mockSession;
const sessionInfo = server.getSessionInfo();
expect(sessionInfo.active).toBe(true);
expect(sessionInfo.sessionId).toBe('sse-session-123');
expect(sessionInfo.age).toBeGreaterThanOrEqual(0);
});
});
describe('Notification handling for stale sessions (#654)', () => {
beforeEach(() => {
// Re-apply mockImplementation after vi.clearAllMocks() resets it
mockConsoleManager.wrapOperation.mockImplementation(async (fn: () => Promise<any>) => {
return await fn();
});
});
it('should return 202 for notification with stale session ID', async () => {
server = new SingleSessionHTTPServer();
const { req, res } = createMockReqRes();
req.headers = { 'mcp-session-id': 'stale-session-that-does-not-exist' };
req.method = 'POST';
req.body = {
jsonrpc: '2.0',
method: 'notifications/initialized',
};
await server.handleRequest(req as any, res as any);
expect(res.status).toHaveBeenCalledWith(202);
expect(res.end).toHaveBeenCalled();
});
it('should return 202 for notification batch with stale session ID', async () => {
server = new SingleSessionHTTPServer();
const { req, res } = createMockReqRes();
req.headers = { 'mcp-session-id': 'stale-session-that-does-not-exist' };
req.method = 'POST';
req.body = [
{ jsonrpc: '2.0', method: 'notifications/initialized' },
{ jsonrpc: '2.0', method: 'notifications/cancelled' },
];
await server.handleRequest(req as any, res as any);
expect(res.status).toHaveBeenCalledWith(202);
expect(res.end).toHaveBeenCalled();
});
it('should return 400 for request (with id) with stale session ID', async () => {
server = new SingleSessionHTTPServer();
const { req, res } = createMockReqRes();
req.headers = { 'mcp-session-id': 'stale-session-that-does-not-exist' };
req.method = 'POST';
req.body = {
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'search_nodes', arguments: { query: 'http' } },
id: 42,
};
await server.handleRequest(req as any, res as any);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
error: expect.objectContaining({
message: 'Bad Request: Session not found or expired',
}),
}));
});
it('should return 202 for notification with no session ID', async () => {
server = new SingleSessionHTTPServer();
const { req, res } = createMockReqRes();
req.method = 'POST';
req.body = {
jsonrpc: '2.0',
method: 'notifications/cancelled',
};
await server.handleRequest(req as any, res as any);
expect(res.status).toHaveBeenCalledWith(202);
expect(res.end).toHaveBeenCalled();
});
it('should return 400 for request with no session ID and not initialize', async () => {
server = new SingleSessionHTTPServer();
const { req, res } = createMockReqRes();
req.method = 'POST';
req.body = {
jsonrpc: '2.0',
method: 'tools/list',
id: 1,
};
await server.handleRequest(req as any, res as any);
expect(res.status).toHaveBeenCalledWith(400);
});
});
});

View File

@@ -0,0 +1,744 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { N8nApiClient } from '@/services/n8n-api-client';
import { N8nApiError } from '@/utils/n8n-errors';
// Mock dependencies
vi.mock('@/services/n8n-api-client');
vi.mock('@/config/n8n-api', () => ({
getN8nApiConfig: vi.fn(),
}));
vi.mock('@/services/n8n-validation', () => ({
validateWorkflowStructure: vi.fn(),
hasWebhookTrigger: vi.fn(),
getWebhookUrl: vi.fn(),
}));
vi.mock('@/utils/logger', () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
},
Logger: vi.fn().mockImplementation(() => ({
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
})),
LogLevel: {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
},
}));
describe('Data Table Handlers (n8n_manage_datatable)', () => {
let mockApiClient: any;
let handlers: any;
let getN8nApiConfig: any;
beforeEach(async () => {
vi.clearAllMocks();
// Setup mock API client with all data table methods
mockApiClient = {
createWorkflow: vi.fn(),
getWorkflow: vi.fn(),
updateWorkflow: vi.fn(),
deleteWorkflow: vi.fn(),
listWorkflows: vi.fn(),
triggerWebhook: vi.fn(),
getExecution: vi.fn(),
listExecutions: vi.fn(),
deleteExecution: vi.fn(),
healthCheck: vi.fn(),
createDataTable: vi.fn(),
listDataTables: vi.fn(),
getDataTable: vi.fn(),
updateDataTable: vi.fn(),
deleteDataTable: vi.fn(),
getDataTableRows: vi.fn(),
insertDataTableRows: vi.fn(),
updateDataTableRows: vi.fn(),
upsertDataTableRow: vi.fn(),
deleteDataTableRows: vi.fn(),
};
// Import mocked modules
getN8nApiConfig = (await import('@/config/n8n-api')).getN8nApiConfig;
// Mock the API config
vi.mocked(getN8nApiConfig).mockReturnValue({
baseUrl: 'https://n8n.test.com',
apiKey: 'test-key',
timeout: 30000,
maxRetries: 3,
});
// Mock the N8nApiClient constructor
vi.mocked(N8nApiClient).mockImplementation(() => mockApiClient);
// Import handlers module after setting up mocks
handlers = await import('@/mcp/handlers-n8n-manager');
});
afterEach(() => {
if (handlers) {
const clientGetter = handlers.getN8nApiClient;
if (clientGetter) {
vi.mocked(getN8nApiConfig).mockReturnValue(null);
clientGetter();
}
}
});
// ========================================================================
// handleCreateTable
// ========================================================================
describe('handleCreateTable', () => {
it('should create data table with name and columns successfully', async () => {
const createdTable = {
id: 'dt-123',
name: 'My Data Table',
columns: [
{ id: 'col-1', name: 'email', type: 'string', index: 0 },
{ id: 'col-2', name: 'age', type: 'number', index: 1 },
],
};
mockApiClient.createDataTable.mockResolvedValue(createdTable);
const result = await handlers.handleCreateTable({
name: 'My Data Table',
columns: [
{ name: 'email', type: 'string' },
{ name: 'age', type: 'number' },
],
});
expect(result).toEqual({
success: true,
data: { id: 'dt-123', name: 'My Data Table' },
message: 'Data table "My Data Table" created with ID: dt-123',
});
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
name: 'My Data Table',
columns: [
{ name: 'email', type: 'string' },
{ name: 'age', type: 'number' },
],
});
});
it('should create data table with name only (no columns)', async () => {
const createdTable = {
id: 'dt-456',
name: 'Empty Table',
};
mockApiClient.createDataTable.mockResolvedValue(createdTable);
const result = await handlers.handleCreateTable({
name: 'Empty Table',
});
expect(result).toEqual({
success: true,
data: { id: 'dt-456', name: 'Empty Table' },
message: 'Data table "Empty Table" created with ID: dt-456',
});
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
name: 'Empty Table',
});
});
it('should return error when API returns empty response (null)', async () => {
mockApiClient.createDataTable.mockResolvedValue(null);
const result = await handlers.handleCreateTable({
name: 'Ghost Table',
});
expect(result).toEqual({
success: false,
error: 'Data table creation failed: n8n API returned an empty or invalid response',
});
});
it('should return error when API call fails', async () => {
const apiError = new Error('Data table creation failed on the server');
mockApiClient.createDataTable.mockRejectedValue(apiError);
const result = await handlers.handleCreateTable({
name: 'Broken Table',
});
expect(result).toEqual({
success: false,
error: 'Data table creation failed on the server',
});
});
it('should return Zod validation error when name is missing', async () => {
const result = await handlers.handleCreateTable({});
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid input');
expect(result.details).toHaveProperty('errors');
});
it('should return error when n8n API is not configured', async () => {
vi.mocked(getN8nApiConfig).mockReturnValue(null);
const result = await handlers.handleCreateTable({
name: 'Test Table',
});
expect(result).toEqual({
success: false,
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
});
});
it('should return structured error for N8nApiError', async () => {
const apiError = new N8nApiError('Feature not available', 402, 'PAYMENT_REQUIRED', { plan: 'enterprise' });
mockApiClient.createDataTable.mockRejectedValue(apiError);
const result = await handlers.handleCreateTable({
name: 'Enterprise Table',
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.code).toBe('PAYMENT_REQUIRED');
expect(result.details).toEqual({ plan: 'enterprise' });
});
it('should return Unknown error when a non-Error value is thrown', async () => {
mockApiClient.createDataTable.mockRejectedValue('string-error');
const result = await handlers.handleCreateTable({
name: 'Error Table',
});
expect(result).toEqual({
success: false,
error: 'Unknown error occurred',
});
});
});
// ========================================================================
// handleListTables
// ========================================================================
describe('handleListTables', () => {
it('should list tables successfully', async () => {
const tables = [
{ id: 'dt-1', name: 'Table One' },
{ id: 'dt-2', name: 'Table Two' },
];
mockApiClient.listDataTables.mockResolvedValue({ data: tables, nextCursor: null });
const result = await handlers.handleListTables({});
expect(result).toEqual({
success: true,
data: {
tables,
count: 2,
nextCursor: undefined,
},
});
});
it('should return empty list when no tables exist', async () => {
mockApiClient.listDataTables.mockResolvedValue({ data: [], nextCursor: null });
const result = await handlers.handleListTables({});
expect(result).toEqual({
success: true,
data: {
tables: [],
count: 0,
nextCursor: undefined,
},
});
});
it('should pass pagination params (limit, cursor)', async () => {
mockApiClient.listDataTables.mockResolvedValue({
data: [{ id: 'dt-3', name: 'Page Two' }],
nextCursor: 'cursor-next',
});
const result = await handlers.handleListTables({ limit: 10, cursor: 'cursor-abc' });
expect(mockApiClient.listDataTables).toHaveBeenCalledWith({ limit: 10, cursor: 'cursor-abc' });
expect(result.success).toBe(true);
expect(result.data.nextCursor).toBe('cursor-next');
});
it('should handle API error', async () => {
mockApiClient.listDataTables.mockRejectedValue(new Error('Server down'));
const result = await handlers.handleListTables({});
expect(result.success).toBe(false);
expect(result.error).toBe('Server down');
});
});
// ========================================================================
// handleGetTable
// ========================================================================
describe('handleGetTable', () => {
it('should get table successfully', async () => {
const table = { id: 'dt-1', name: 'My Table', columns: [] };
mockApiClient.getDataTable.mockResolvedValue(table);
const result = await handlers.handleGetTable({ tableId: 'dt-1' });
expect(result).toEqual({
success: true,
data: table,
});
expect(mockApiClient.getDataTable).toHaveBeenCalledWith('dt-1');
});
it('should return error on 404', async () => {
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
mockApiClient.getDataTable.mockRejectedValue(notFoundError);
const result = await handlers.handleGetTable({ tableId: 'dt-nonexistent' });
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.code).toBe('NOT_FOUND');
});
it('should return Zod validation error when tableId is missing', async () => {
const result = await handlers.handleGetTable({});
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid input');
expect(result.details).toHaveProperty('errors');
});
});
// ========================================================================
// handleUpdateTable
// ========================================================================
describe('handleUpdateTable', () => {
it('should rename table successfully', async () => {
const updatedTable = { id: 'dt-1', name: 'Renamed Table' };
mockApiClient.updateDataTable.mockResolvedValue(updatedTable);
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'Renamed Table' });
expect(result).toEqual({
success: true,
data: updatedTable,
message: 'Data table renamed to "Renamed Table"',
});
expect(mockApiClient.updateDataTable).toHaveBeenCalledWith('dt-1', { name: 'Renamed Table' });
});
it('should return Zod validation error when tableId is missing', async () => {
const result = await handlers.handleUpdateTable({ name: 'New Name' });
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid input');
expect(result.details).toHaveProperty('errors');
});
it('should return error when API call fails', async () => {
mockApiClient.updateDataTable.mockRejectedValue(new Error('Update failed'));
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'New Name' });
expect(result.success).toBe(false);
expect(result.error).toBe('Update failed');
});
it('should warn when columns parameter is passed', async () => {
const updatedTable = { id: 'dt-1', name: 'Renamed' };
mockApiClient.updateDataTable.mockResolvedValue(updatedTable);
const result = await handlers.handleUpdateTable({
tableId: 'dt-1',
name: 'Renamed',
columns: [{ name: 'phone', type: 'string' }],
});
expect(result.success).toBe(true);
expect(result.message).toContain('columns parameter was ignored');
expect(result.message).toContain('immutable after creation');
expect(mockApiClient.updateDataTable).toHaveBeenCalledWith('dt-1', { name: 'Renamed' });
});
});
// ========================================================================
// handleDeleteTable
// ========================================================================
describe('handleDeleteTable', () => {
it('should delete table successfully', async () => {
mockApiClient.deleteDataTable.mockResolvedValue(undefined);
const result = await handlers.handleDeleteTable({ tableId: 'dt-1' });
expect(result).toEqual({
success: true,
message: 'Data table dt-1 deleted successfully',
});
expect(mockApiClient.deleteDataTable).toHaveBeenCalledWith('dt-1');
});
it('should return error on 404', async () => {
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
mockApiClient.deleteDataTable.mockRejectedValue(notFoundError);
const result = await handlers.handleDeleteTable({ tableId: 'dt-nonexistent' });
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.code).toBe('NOT_FOUND');
});
});
// ========================================================================
// handleGetRows
// ========================================================================
describe('handleGetRows', () => {
it('should get rows with default params', async () => {
const rows = [
{ id: 1, email: 'a@b.com', score: 10 },
{ id: 2, email: 'c@d.com', score: 20 },
];
mockApiClient.getDataTableRows.mockResolvedValue({ data: rows, nextCursor: null });
const result = await handlers.handleGetRows({ tableId: 'dt-1' });
expect(result).toEqual({
success: true,
data: {
rows,
count: 2,
nextCursor: undefined,
},
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {});
});
it('should pass filter, sort, and search params', async () => {
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
await handlers.handleGetRows({
tableId: 'dt-1',
limit: 50,
sortBy: 'name:asc',
search: 'john',
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
limit: 50,
sortBy: 'name:asc',
search: 'john',
});
});
it('should serialize object filter to JSON string', async () => {
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
const objectFilter = {
type: 'and' as const,
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'active' }],
};
await handlers.handleGetRows({
tableId: 'dt-1',
filter: objectFilter,
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify(objectFilter),
});
});
it('should pass string filter as-is', async () => {
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
const filterStr = '{"type":"and","filters":[]}';
await handlers.handleGetRows({
tableId: 'dt-1',
filter: filterStr,
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: filterStr,
});
});
});
// ========================================================================
// handleInsertRows
// ========================================================================
describe('handleInsertRows', () => {
it('should insert rows successfully', async () => {
const insertResult = { insertedCount: 2, ids: [1, 2] };
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
const result = await handlers.handleInsertRows({
tableId: 'dt-1',
data: [
{ email: 'a@b.com', score: 10 },
{ email: 'c@d.com', score: 20 },
],
});
expect(result).toEqual({
success: true,
data: insertResult,
message: 'Rows inserted into data table dt-1',
});
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
data: [
{ email: 'a@b.com', score: 10 },
{ email: 'c@d.com', score: 20 },
],
});
});
it('should pass returnType to the API client', async () => {
const insertResult = [{ id: 1, email: 'a@b.com', score: 10 }];
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
const result = await handlers.handleInsertRows({
tableId: 'dt-1',
data: [{ email: 'a@b.com', score: 10 }],
returnType: 'all',
});
expect(result.success).toBe(true);
expect(result.data).toEqual(insertResult);
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
data: [{ email: 'a@b.com', score: 10 }],
returnType: 'all',
});
});
it('should return Zod validation error when data is empty array', async () => {
const result = await handlers.handleInsertRows({
tableId: 'dt-1',
data: [],
});
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid input');
expect(result.details).toHaveProperty('errors');
});
});
// ========================================================================
// handleUpdateRows
// ========================================================================
describe('handleUpdateRows', () => {
it('should update rows successfully', async () => {
const updateResult = { updatedCount: 3 };
mockApiClient.updateDataTableRows.mockResolvedValue(updateResult);
const filter = {
type: 'and' as const,
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'inactive' }],
};
const result = await handlers.handleUpdateRows({
tableId: 'dt-1',
filter,
data: { status: 'active' },
});
expect(result).toEqual({
success: true,
data: updateResult,
message: 'Rows updated successfully',
});
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
filter,
data: { status: 'active' },
});
});
it('should support dryRun mode', async () => {
const dryRunResult = { matchedCount: 5 };
mockApiClient.updateDataTableRows.mockResolvedValue(dryRunResult);
const filter = {
filters: [{ columnName: 'score', condition: 'lt' as const, value: 5 }],
};
const result = await handlers.handleUpdateRows({
tableId: 'dt-1',
filter,
data: { status: 'low' },
dryRun: true,
});
expect(result.success).toBe(true);
expect(result.message).toBe('Dry run: rows matched (no changes applied)');
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: { type: 'and', ...filter },
data: { status: 'low' },
dryRun: true,
});
});
it('should return error on API failure', async () => {
mockApiClient.updateDataTableRows.mockRejectedValue(new Error('Conflict'));
const result = await handlers.handleUpdateRows({
tableId: 'dt-1',
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
data: { name: 'test' },
});
expect(result.success).toBe(false);
expect(result.error).toBe('Conflict');
});
});
// ========================================================================
// handleUpsertRows
// ========================================================================
describe('handleUpsertRows', () => {
it('should upsert row successfully', async () => {
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com', score: 15 } };
mockApiClient.upsertDataTableRow.mockResolvedValue(upsertResult);
const filter = {
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
};
const result = await handlers.handleUpsertRows({
tableId: 'dt-1',
filter,
data: { score: 15 },
});
expect(result).toEqual({
success: true,
data: upsertResult,
message: 'Row upserted successfully',
});
expect(mockApiClient.upsertDataTableRow).toHaveBeenCalledWith('dt-1', {
filter: { type: 'and', ...filter },
data: { score: 15 },
});
});
it('should support dryRun mode', async () => {
const dryRunResult = { action: 'would_update', matchedRows: 1 };
mockApiClient.upsertDataTableRow.mockResolvedValue(dryRunResult);
const filter = {
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
};
const result = await handlers.handleUpsertRows({
tableId: 'dt-1',
filter,
data: { score: 20 },
dryRun: true,
});
expect(result.success).toBe(true);
expect(result.message).toBe('Dry run: upsert previewed (no changes applied)');
});
it('should return error on API failure', async () => {
const apiError = new N8nApiError('Server error', 500, 'INTERNAL_ERROR');
mockApiClient.upsertDataTableRow.mockRejectedValue(apiError);
const result = await handlers.handleUpsertRows({
tableId: 'dt-1',
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
data: { name: 'test' },
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.code).toBe('INTERNAL_ERROR');
});
});
// ========================================================================
// handleDeleteRows
// ========================================================================
describe('handleDeleteRows', () => {
it('should delete rows successfully', async () => {
const deleteResult = { deletedCount: 2 };
mockApiClient.deleteDataTableRows.mockResolvedValue(deleteResult);
const filter = {
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'deleted' }],
};
const result = await handlers.handleDeleteRows({
tableId: 'dt-1',
filter,
});
expect(result).toEqual({
success: true,
data: deleteResult,
message: 'Rows deleted successfully',
});
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify({ type: 'and', ...filter }),
});
});
it('should serialize filter to JSON string for API call', async () => {
mockApiClient.deleteDataTableRows.mockResolvedValue({ deletedCount: 1 });
const filter = {
type: 'or' as const,
filters: [
{ columnName: 'score', condition: 'lt' as const, value: 0 },
{ columnName: 'status', condition: 'eq' as const, value: 'spam' },
],
};
await handlers.handleDeleteRows({ tableId: 'dt-1', filter });
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify(filter),
});
});
it('should support dryRun mode', async () => {
const dryRunResult = { matchedCount: 4 };
mockApiClient.deleteDataTableRows.mockResolvedValue(dryRunResult);
const filter = {
filters: [{ columnName: 'active', condition: 'eq' as const, value: false }],
};
const result = await handlers.handleDeleteRows({
tableId: 'dt-1',
filter,
dryRun: true,
});
expect(result.success).toBe(true);
expect(result.message).toBe('Dry run: rows matched for deletion (no changes applied)');
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify({ type: 'and', ...filter }),
dryRun: true,
});
});
});
});

View File

@@ -100,6 +100,16 @@ describe('handlers-n8n-manager', () => {
listExecutions: vi.fn(),
deleteExecution: vi.fn(),
healthCheck: vi.fn(),
createDataTable: vi.fn(),
listDataTables: vi.fn(),
getDataTable: vi.fn(),
updateDataTable: vi.fn(),
deleteDataTable: vi.fn(),
getDataTableRows: vi.fn(),
insertDataTableRows: vi.fn(),
updateDataTableRows: vi.fn(),
upsertDataTableRow: vi.fn(),
deleteDataTableRows: vi.fn(),
};
// Setup mock repository
@@ -631,6 +641,27 @@ describe('handlers-n8n-manager', () => {
expect(result.details.errors[0]).toContain('Webhook');
});
});
it('should pass projectId to API when provided', async () => {
const testWorkflow = createTestWorkflow();
const input = {
name: 'Test Workflow',
nodes: testWorkflow.nodes,
connections: testWorkflow.connections,
projectId: 'project-abc-123',
};
mockApiClient.createWorkflow.mockResolvedValue(testWorkflow);
const result = await handlers.handleCreateWorkflow(input);
expect(result.success).toBe(true);
expect(mockApiClient.createWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
projectId: 'project-abc-123',
})
);
});
});
describe('handleGetWorkflow', () => {
@@ -1081,10 +1112,10 @@ describe('handlers-n8n-manager', () => {
enabled: true,
},
managementTools: {
count: 13,
count: 14,
enabled: true,
},
totalAvailable: 20,
totalAvailable: 21,
},
});

View File

@@ -542,6 +542,9 @@ describe('Parameter Validation', () => {
await expect(server.testExecuteTool('n8n_test_workflow', {}))
.rejects.toThrow('Missing required parameters for n8n_test_workflow: workflowId');
await expect(server.testExecuteTool('n8n_manage_datatable', {}))
.rejects.toThrow('n8n_manage_datatable: Validation failed:\n • action: action is required');
for (const tool of n8nToolsWithRequiredParams) {
await expect(server.testExecuteTool(tool.name, tool.args))
.rejects.toThrow(tool.expected);

View File

@@ -297,7 +297,7 @@ describe('N8nApiClient', () => {
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).message.toLowerCase()).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
@@ -380,7 +380,7 @@ describe('N8nApiClient', () => {
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).message.toLowerCase()).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
@@ -432,7 +432,7 @@ describe('N8nApiClient', () => {
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).message.toLowerCase()).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
@@ -501,7 +501,7 @@ describe('N8nApiClient', () => {
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).message.toLowerCase()).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
@@ -1278,7 +1278,7 @@ describe('N8nApiClient', () => {
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).message.toLowerCase()).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
@@ -1300,6 +1300,363 @@ describe('N8nApiClient', () => {
});
});
describe('createDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should create data table with name and columns', async () => {
const params = {
name: 'My Table',
columns: [
{ name: 'email', type: 'string' as const },
{ name: 'count', type: 'number' as const },
],
};
const createdTable = { id: 'dt-1', name: 'My Table', columns: [] };
mockAxiosInstance.post.mockResolvedValue({ data: createdTable });
const result = await client.createDataTable(params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables', params);
expect(result).toEqual(createdTable);
});
it('should create data table without columns', async () => {
const params = { name: 'Empty Table' };
const createdTable = { id: 'dt-2', name: 'Empty Table' };
mockAxiosInstance.post.mockResolvedValue({ data: createdTable });
const result = await client.createDataTable(params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables', params);
expect(result).toEqual(createdTable);
});
it('should handle 400 error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid table name' } },
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.createDataTable({ name: '' });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toBe('Invalid table name');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('listDataTables', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should list data tables successfully', async () => {
const response = { data: [{ id: 'dt-1', name: 'Table One' }], nextCursor: 'abc' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listDataTables({ limit: 10, cursor: 'xyz' });
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables', { params: { limit: 10, cursor: 'xyz' } });
expect(result).toEqual(response);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.listDataTables();
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('getDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should get data table successfully', async () => {
const table = { id: 'dt-1', name: 'My Table', columns: [] };
mockAxiosInstance.get.mockResolvedValue({ data: table });
const result = await client.getDataTable('dt-1');
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1');
expect(result).toEqual(table);
});
it('should handle 404 error', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Data table not found' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.getDataTable('dt-nonexistent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
});
describe('updateDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should update data table successfully', async () => {
const updated = { id: 'dt-1', name: 'Renamed' };
mockAxiosInstance.patch.mockResolvedValue({ data: updated });
const result = await client.updateDataTable('dt-1', { name: 'Renamed' });
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1', { name: 'Renamed' });
expect(result).toEqual(updated);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid name' } },
};
await mockAxiosInstance.simulateError('patch', error);
try {
await client.updateDataTable('dt-1', { name: '' });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('deleteDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should delete data table successfully', async () => {
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
await client.deleteDataTable('dt-1');
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1');
});
it('should handle 404 error', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Data table not found' } },
};
await mockAxiosInstance.simulateError('delete', error);
try {
await client.deleteDataTable('dt-nonexistent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
});
describe('getDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should get data table rows with params', async () => {
const response = { data: [{ id: 1, email: 'a@b.com' }], nextCursor: null };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const params = { limit: 50, sortBy: 'email:asc', search: 'john' };
const result = await client.getDataTableRows('dt-1', params);
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1/rows', expect.objectContaining({ params }));
expect(result).toEqual(response);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.getDataTableRows('dt-1');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('insertDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should insert data table rows successfully', async () => {
const insertResult = { insertedCount: 2 };
mockAxiosInstance.post.mockResolvedValue({ data: insertResult });
const params = { data: [{ email: 'a@b.com' }, { email: 'c@d.com' }], returnType: 'count' as const };
const result = await client.insertDataTableRows('dt-1', params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows', params);
expect(result).toEqual(insertResult);
});
it('should handle 400 error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid row data' } },
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.insertDataTableRows('dt-1', { data: [{}] });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toBe('Invalid row data');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('updateDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should update data table rows successfully', async () => {
const updateResult = { updatedCount: 3 };
mockAxiosInstance.patch.mockResolvedValue({ data: updateResult });
const params = {
filter: { type: 'and' as const, filters: [{ columnName: 'status', condition: 'eq' as const, value: 'old' }] },
data: { status: 'new' },
};
const result = await client.updateDataTableRows('dt-1', params);
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1/rows/update', params);
expect(result).toEqual(updateResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('patch', error);
try {
await client.updateDataTableRows('dt-1', {
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
data: { name: 'test' },
});
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('upsertDataTableRow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should upsert data table row successfully', async () => {
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com' } };
mockAxiosInstance.post.mockResolvedValue({ data: upsertResult });
const params = {
filter: { type: 'and' as const, filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }] },
data: { score: 15 },
};
const result = await client.upsertDataTableRow('dt-1', params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows/upsert', params);
expect(result).toEqual(upsertResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid upsert params' } },
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.upsertDataTableRow('dt-1', {
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
data: { name: 'test' },
});
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('deleteDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should delete data table rows successfully', async () => {
const deleteResult = { deletedCount: 2 };
mockAxiosInstance.delete.mockResolvedValue({ data: deleteResult });
const params = { filter: '{"type":"and","filters":[]}', dryRun: false };
const result = await client.deleteDataTableRows('dt-1', params);
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1/rows/delete', expect.objectContaining({ params }));
expect(result).toEqual(deleteResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('delete', error);
try {
await client.deleteDataTableRows('dt-1', { filter: '{}' });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('interceptors', () => {
let requestInterceptor: any;
let responseInterceptor: any;