mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
feat(core): Add MCP tool to read data table rows (#35956)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
dc55ed1e9e
commit
de944d21d1
|
|
@ -214,9 +214,11 @@ export {
|
|||
dataTableNameSchema,
|
||||
} from './schemas/data-table.schema';
|
||||
|
||||
export type {
|
||||
DataTableFilter,
|
||||
DataTableFilterConditionType,
|
||||
export {
|
||||
type DataTableFilter,
|
||||
type DataTableFilterConditionType,
|
||||
FilterConditionSchema,
|
||||
dataTableFilterTypeSchema,
|
||||
} from './schemas/data-table-filter.schema';
|
||||
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import { createTeamProject, testDb, testModules } from '@n8n/backend-test-utils';
|
||||
import { GLOBAL_OWNER_ROLE, type Project, type User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { DataTableProxyService } from '@/modules/data-table/data-table-proxy.service';
|
||||
import { DataTableService } from '@/modules/data-table/data-table.service';
|
||||
import { createUser } from '@test-integration/db/users';
|
||||
|
||||
import { createTelemetry } from './test-utils';
|
||||
import { createGetDataTableRowsTool } from '../../tools/data-table';
|
||||
|
||||
beforeAll(async () => {
|
||||
await testModules.loadModules(['data-table']);
|
||||
await testDb.init();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['DataTable', 'DataTableColumn']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('get_data_table_rows against a real database', () => {
|
||||
let user: User;
|
||||
let project: Project;
|
||||
let dataTableId: string;
|
||||
let insertedIds: number[];
|
||||
let tool: ReturnType<typeof createGetDataTableRowsTool>;
|
||||
|
||||
const callHandler = async (args: Omit<Parameters<typeof tool.handler>[0], 'dataTableId'>) =>
|
||||
await tool.handler({ dataTableId, ...args }, {} as never);
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
project = await createTeamProject(undefined, user);
|
||||
const dataTableOps = Container.get(DataTableProxyService).makeDataTableOperationsForUser(user);
|
||||
tool = createGetDataTableRowsTool(user, dataTableOps, createTelemetry());
|
||||
|
||||
const created = await Container.get(DataTableService).createDataTable(project.id, {
|
||||
name: 'people',
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
{ name: 'joinedAt', type: 'date' },
|
||||
],
|
||||
});
|
||||
dataTableId = created.id;
|
||||
|
||||
const inserted = await dataTableOps.insertRows(
|
||||
dataTableId,
|
||||
project.id,
|
||||
[
|
||||
{ name: 'Alice', age: 30, joinedAt: '2024-03-01T00:00:00.000Z' },
|
||||
{ name: 'Bob', age: 25, joinedAt: null },
|
||||
{ name: 'Carol', age: 35, joinedAt: '2024-05-01T00:00:00.000Z' },
|
||||
],
|
||||
'id',
|
||||
);
|
||||
insertedIds = inserted.map((row) => row.id);
|
||||
});
|
||||
|
||||
it('filters on the id system column', async () => {
|
||||
const result = await callHandler({
|
||||
projectId: project.id,
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'id', condition: 'gt', value: insertedIds[0] }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const output = result.structuredContent as { rows: Array<Record<string, unknown>> };
|
||||
expect(output.rows.map((row) => row.name)).toEqual(['Bob', 'Carol']);
|
||||
});
|
||||
|
||||
it('matches null values with eq', async () => {
|
||||
const result = await callHandler({
|
||||
projectId: project.id,
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'joinedAt', condition: 'eq', value: null }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const output = result.structuredContent as { rows: Array<Record<string, unknown>> };
|
||||
expect(output.rows.map((row) => row.name)).toEqual(['Bob']);
|
||||
});
|
||||
|
||||
it('filters date columns by ISO 8601 string', async () => {
|
||||
const result = await callHandler({
|
||||
projectId: project.id,
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'joinedAt', condition: 'gte', value: '2024-04-01T00:00:00.000Z' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const output = result.structuredContent as { rows: Array<Record<string, unknown>> };
|
||||
expect(output.rows.map((row) => row.name)).toEqual(['Carol']);
|
||||
});
|
||||
|
||||
it('sorts descending and paginates', async () => {
|
||||
const result = await callHandler({
|
||||
projectId: project.id,
|
||||
sortBy: 'age:desc',
|
||||
limit: 2,
|
||||
skip: 1,
|
||||
});
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const output = result.structuredContent as {
|
||||
rows: Array<Record<string, unknown>>;
|
||||
count: number;
|
||||
};
|
||||
expect(output.rows.map((row) => row.name)).toEqual(['Alice', 'Bob']);
|
||||
expect(output.count).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import z from 'zod';
|
||||
|
||||
import type { DataTableUserOperations } from '@/modules/data-table/data-table-proxy.service';
|
||||
|
||||
import { createTelemetry, user } from './test-utils';
|
||||
import { createGetDataTableRowsTool } from '../../tools/data-table';
|
||||
|
||||
const createdAt = new Date('2024-01-01T00:00:00.000Z');
|
||||
const updatedAt = new Date('2024-01-02T00:00:00.000Z');
|
||||
|
||||
const sampleRows = [
|
||||
{ id: 1, name: 'Alice', age: 30, createdAt, updatedAt },
|
||||
{ id: 2, name: 'Bob', age: 25, createdAt, updatedAt },
|
||||
];
|
||||
|
||||
const createMocks = (overrides?: {
|
||||
rows?: Array<Record<string, unknown>>;
|
||||
count?: number;
|
||||
error?: Error;
|
||||
}) => {
|
||||
const dataTableOps = {
|
||||
getManyRowsAndCount: overrides?.error
|
||||
? vi.fn().mockRejectedValue(overrides.error)
|
||||
: vi.fn().mockResolvedValue({
|
||||
data: overrides?.rows ?? sampleRows,
|
||||
count: overrides?.count ?? sampleRows.length,
|
||||
}),
|
||||
} as unknown as DataTableUserOperations;
|
||||
|
||||
const telemetry = createTelemetry();
|
||||
|
||||
return { dataTableOps, telemetry };
|
||||
};
|
||||
|
||||
const callHandler = async (
|
||||
tool: ReturnType<typeof createGetDataTableRowsTool>,
|
||||
args: Parameters<typeof tool.handler>[0],
|
||||
) => await tool.handler(args, {} as never);
|
||||
|
||||
describe('get_data_table_rows MCP tool', () => {
|
||||
test('creates tool correctly', () => {
|
||||
const { dataTableOps, telemetry } = createMocks();
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
expect(tool.name).toBe('get_data_table_rows');
|
||||
expect(tool.config).toBeDefined();
|
||||
expect(typeof tool.config.description).toBe('string');
|
||||
expect(tool.config.inputSchema).toBeDefined();
|
||||
expect(tool.config.outputSchema).toBeDefined();
|
||||
expect(tool.config.annotations?.readOnlyHint).toBe(true);
|
||||
expect(typeof tool.handler).toBe('function');
|
||||
});
|
||||
|
||||
test('returns rows with dates serialized to ISO strings', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks();
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
const result = await callHandler(tool, { dataTableId: 'dt-1', projectId: 'proj-1' });
|
||||
|
||||
expect(result.structuredContent).toEqual({
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-02T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob',
|
||||
age: 25,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-02T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
});
|
||||
|
||||
expect(dataTableOps.getManyRowsAndCount).toHaveBeenCalledWith('dt-1', 'proj-1', {
|
||||
take: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('passes filter, sort and pagination options through', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks();
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
await callHandler(tool, {
|
||||
dataTableId: 'dt-1',
|
||||
projectId: 'proj-1',
|
||||
filter: {
|
||||
type: 'or',
|
||||
filters: [
|
||||
{ columnName: 'name', condition: 'ilike', value: 'ali' },
|
||||
{ columnName: 'age', condition: 'gte', value: 21 },
|
||||
],
|
||||
},
|
||||
sortBy: 'age:desc',
|
||||
limit: 10,
|
||||
skip: 5,
|
||||
});
|
||||
|
||||
expect(dataTableOps.getManyRowsAndCount).toHaveBeenCalledWith('dt-1', 'proj-1', {
|
||||
filter: {
|
||||
type: 'or',
|
||||
filters: [
|
||||
{ columnName: 'name', condition: 'ilike', value: 'ali' },
|
||||
{ columnName: 'age', condition: 'gte', value: 21 },
|
||||
],
|
||||
},
|
||||
sortBy: ['age', 'DESC'],
|
||||
take: 10,
|
||||
skip: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('defaults filter type to and, condition to eq', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks();
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
// Defaults are applied when the registration layer parses args against
|
||||
// the input schema, so parse here the same way before calling the handler.
|
||||
const args = z.object(tool.config.inputSchema!).parse({
|
||||
dataTableId: 'dt-1',
|
||||
projectId: 'proj-1',
|
||||
filter: { filters: [{ columnName: 'name', value: 'Alice' }] },
|
||||
sortBy: 'name:asc',
|
||||
});
|
||||
await callHandler(tool, args);
|
||||
|
||||
expect(dataTableOps.getManyRowsAndCount).toHaveBeenCalledWith('dt-1', 'proj-1', {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', condition: 'eq', value: 'Alice' }],
|
||||
},
|
||||
sortBy: ['name', 'ASC'],
|
||||
take: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns error response on failure', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks({
|
||||
error: new Error('Unknown column'),
|
||||
});
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
const result = await callHandler(tool, { dataTableId: 'dt-1', projectId: 'proj-1' });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.structuredContent).toEqual({
|
||||
rows: [],
|
||||
count: 0,
|
||||
error: 'Unknown column',
|
||||
});
|
||||
});
|
||||
|
||||
// MCP clients validate structuredContent against the advertised output
|
||||
// schema with additionalProperties forbidden, so both response shapes must
|
||||
// parse strictly or the client rejects the response outright.
|
||||
test('success and error responses match the declared output schema', async () => {
|
||||
const strictOutputSchema = z
|
||||
.object(
|
||||
createGetDataTableRowsTool(user, createMocks().dataTableOps, createTelemetry()).config
|
||||
.outputSchema!,
|
||||
)
|
||||
.strict();
|
||||
|
||||
const success = createMocks();
|
||||
const successResult = await callHandler(
|
||||
createGetDataTableRowsTool(user, success.dataTableOps, success.telemetry),
|
||||
{ dataTableId: 'dt-1', projectId: 'proj-1' },
|
||||
);
|
||||
expect(() => strictOutputSchema.parse(successResult.structuredContent)).not.toThrow();
|
||||
|
||||
const failure = createMocks({ error: new Error('Unknown column') });
|
||||
const errorResult = await callHandler(
|
||||
createGetDataTableRowsTool(user, failure.dataTableOps, failure.telemetry),
|
||||
{ dataTableId: 'dt-1', projectId: 'proj-1' },
|
||||
);
|
||||
expect(() => strictOutputSchema.parse(errorResult.structuredContent)).not.toThrow();
|
||||
});
|
||||
|
||||
test('tracks telemetry on success', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks();
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
await callHandler(tool, { dataTableId: 'dt-1', projectId: 'proj-1' });
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
'User called mcp tool',
|
||||
expect.objectContaining({
|
||||
user_id: 'user-1',
|
||||
tool_name: 'get_data_table_rows',
|
||||
results: { success: true, data: { count: 2, returned: 2 } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks telemetry on error', async () => {
|
||||
const { dataTableOps, telemetry } = createMocks({
|
||||
error: new Error('Unknown column'),
|
||||
});
|
||||
const tool = createGetDataTableRowsTool(user, dataTableOps, telemetry);
|
||||
|
||||
await callHandler(tool, { dataTableId: 'dt-1', projectId: 'proj-1' });
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
'User called mcp tool',
|
||||
expect.objectContaining({
|
||||
user_id: 'user-1',
|
||||
tool_name: 'get_data_table_rows',
|
||||
results: { success: false, error: 'Unknown column' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -73,7 +73,7 @@ export const TOOLS_BY_SCOPE: Record<McpScope, readonly string[]> = {
|
|||
// explore_node_resources queries external services with stored credentials,
|
||||
// so it must sit behind the credential scope rather than a workflow one.
|
||||
'credential:read': ['list_credentials', 'list_n8n_connect_services', 'explore_node_resources'],
|
||||
'dataTable:read': ['search_data_tables'],
|
||||
'dataTable:read': ['search_data_tables', 'get_data_table_rows'],
|
||||
// Writing requires finding tables, so search rides along.
|
||||
'dataTable:write': [
|
||||
'search_data_tables',
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ import {
|
|||
createAddDataTableRowsTool,
|
||||
createCreateDataTableTool,
|
||||
createDeleteDataTableColumnTool,
|
||||
createGetDataTableRowsTool,
|
||||
createRenameDataTableColumnTool,
|
||||
createRenameDataTableTool,
|
||||
createSearchDataTablesTool,
|
||||
|
|
@ -591,6 +592,9 @@ export class McpService {
|
|||
const addDataTableRowsTool = createAddDataTableRowsTool(user, dataTableOps, this.telemetry);
|
||||
registerIfAllowed(addDataTableRowsTool);
|
||||
|
||||
const getDataTableRowsTool = createGetDataTableRowsTool(user, dataTableOps, this.telemetry);
|
||||
registerIfAllowed(getDataTableRowsTool);
|
||||
|
||||
// Workflow builder tools (enabled via N8N_MCP_BUILDER_ENABLED)
|
||||
if (builderEnabled) {
|
||||
await this.registerBuilderTools(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { Telemetry } from '@/telemetry';
|
|||
|
||||
import { USER_CALLED_MCP_TOOL_EVENT } from '../../mcp.constants';
|
||||
import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../../mcp.types';
|
||||
import { dataTableProjectIdSchema } from '../schemas';
|
||||
import { dataTableProjectIdSchema, dataTableRowSchema } from '../schemas';
|
||||
|
||||
const ADD_ROWS_MAX = 1000;
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ const addRowsInputSchema = {
|
|||
dataTableId: z.string().describe('The ID of the data table to insert rows into'),
|
||||
projectId: dataTableProjectIdSchema,
|
||||
rows: z
|
||||
.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])))
|
||||
.array(dataTableRowSchema)
|
||||
.min(1)
|
||||
.max(ADD_ROWS_MAX)
|
||||
.describe(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
import {
|
||||
dataTableColumnNameSchema,
|
||||
dataTableFilterTypeSchema,
|
||||
FilterConditionSchema,
|
||||
} from '@n8n/api-types';
|
||||
import type { User } from '@n8n/db';
|
||||
import z from 'zod';
|
||||
|
||||
import type { DataTableUserOperations } from '@/modules/data-table/data-table-proxy.service';
|
||||
import type { Telemetry } from '@/telemetry';
|
||||
|
||||
import { USER_CALLED_MCP_TOOL_EVENT } from '../../mcp.constants';
|
||||
import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../../mcp.types';
|
||||
import { createLimitSchema, dataTableProjectIdSchema, dataTableRowSchema } from '../schemas';
|
||||
|
||||
const GET_ROWS_MAX = 100;
|
||||
|
||||
const filterSchema = z
|
||||
.object({
|
||||
type: dataTableFilterTypeSchema
|
||||
.default('and')
|
||||
.describe('How to combine the filters: all must match (and) or any may match (or)'),
|
||||
filters: z
|
||||
.array(
|
||||
z.object({
|
||||
columnName: dataTableColumnNameSchema.describe(
|
||||
"Column to filter on. System columns 'id', 'createdAt' and 'updatedAt' are also allowed",
|
||||
),
|
||||
condition: FilterConditionSchema.default('eq').describe(
|
||||
"Comparison operator. 'like' (case-sensitive) and 'ilike' (case-insensitive) match substrings; include % wildcards for custom patterns. 'neq' also matches rows where the column is null",
|
||||
),
|
||||
value: z
|
||||
.union([z.string(), z.number(), z.boolean(), z.null()])
|
||||
.describe(
|
||||
'Value to compare against. For date columns, pass an ISO 8601 string. Pass null with eq/neq to match rows where the column is null / not null',
|
||||
),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
})
|
||||
.optional()
|
||||
.describe('Filter conditions to select rows. Omit to return all rows');
|
||||
|
||||
const sortBySchema = z
|
||||
.string()
|
||||
.regex(/^[^:]+:(asc|desc)$/i, 'Expected format <columnName>:<asc|desc>')
|
||||
.refine((value) => dataTableColumnNameSchema.safeParse(value.split(':')[0]).success, {
|
||||
message: 'Invalid sort column name',
|
||||
})
|
||||
.optional()
|
||||
.describe("Sort order as '<columnName>:<asc|desc>', e.g. 'createdAt:desc'");
|
||||
|
||||
const getRowsInputSchema = {
|
||||
dataTableId: z.string().describe('The ID of the data table to read rows from'),
|
||||
projectId: dataTableProjectIdSchema,
|
||||
filter: filterSchema,
|
||||
sortBy: sortBySchema,
|
||||
limit: createLimitSchema(GET_ROWS_MAX),
|
||||
skip: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe('Number of rows to skip, for paginating through large result sets'),
|
||||
} satisfies z.ZodRawShape;
|
||||
|
||||
const getRowsOutputSchema = {
|
||||
rows: z
|
||||
.array(dataTableRowSchema)
|
||||
.describe(
|
||||
"Matching rows, mapping column names to values. Includes the system columns 'id', 'createdAt' and 'updatedAt'; dates are ISO 8601 strings",
|
||||
),
|
||||
count: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe('Total number of rows matching the filter, ignoring limit and skip'),
|
||||
error: z.string().optional().describe('Error message if the read failed'),
|
||||
} satisfies z.ZodRawShape;
|
||||
|
||||
const toSortByTuple = (sortBy: string): [string, 'ASC' | 'DESC'] => {
|
||||
const [column, direction] = sortBy.split(':');
|
||||
return [column, direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'];
|
||||
};
|
||||
|
||||
const serializeRow = (row: Record<string, unknown>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(row).map(([key, value]) => [
|
||||
key,
|
||||
value instanceof Date ? value.toISOString() : value,
|
||||
]),
|
||||
);
|
||||
|
||||
export const createGetDataTableRowsTool = (
|
||||
user: User,
|
||||
dataTableOps: DataTableUserOperations,
|
||||
telemetry: Telemetry,
|
||||
): ToolDefinition<typeof getRowsInputSchema> => ({
|
||||
name: 'get_data_table_rows',
|
||||
config: {
|
||||
description:
|
||||
'Read rows from a data table, with optional filtering, sorting and pagination. Use search_data_tables to find the data table ID and its columns first.',
|
||||
inputSchema: getRowsInputSchema,
|
||||
outputSchema: getRowsOutputSchema,
|
||||
annotations: {
|
||||
title: 'Get Data Table Rows',
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
handler: async ({ dataTableId, projectId, filter, sortBy, limit = GET_ROWS_MAX, skip }) => {
|
||||
const telemetryPayload: UserCalledMCPToolEventPayload = {
|
||||
user_id: user.id,
|
||||
tool_name: 'get_data_table_rows',
|
||||
parameters: {
|
||||
dataTableId,
|
||||
projectId,
|
||||
filterCount: filter?.filters.length,
|
||||
sortBy,
|
||||
limit,
|
||||
skip,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await dataTableOps.getManyRowsAndCount(dataTableId, projectId, {
|
||||
filter,
|
||||
sortBy: sortBy ? toSortByTuple(sortBy) : undefined,
|
||||
take: limit,
|
||||
skip,
|
||||
});
|
||||
|
||||
const output = {
|
||||
rows: result.data.map(serializeRow),
|
||||
count: result.count,
|
||||
};
|
||||
|
||||
telemetryPayload.results = {
|
||||
success: true,
|
||||
data: { count: result.count, returned: output.rows.length },
|
||||
};
|
||||
telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
telemetryPayload.results = {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
|
||||
|
||||
const output = { rows: [], count: 0, error: errorMessage };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -5,3 +5,4 @@ export { createAddDataTableColumnTool } from './add-data-table-column.tool';
|
|||
export { createDeleteDataTableColumnTool } from './delete-data-table-column.tool';
|
||||
export { createRenameDataTableColumnTool } from './rename-data-table-column.tool';
|
||||
export { createAddDataTableRowsTool } from './add-data-table-rows.tool';
|
||||
export { createGetDataTableRowsTool } from './get-data-table-rows.tool';
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ export const columnNameSchema = z
|
|||
'Column name. Must start with a letter, contain only letters, numbers, and underscores (max 63 chars)',
|
||||
);
|
||||
|
||||
export const dataTableRowSchema = z.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
||||
);
|
||||
|
||||
export const successMessageOutputSchema = {
|
||||
success: z.boolean().describe('Whether the operation succeeded'),
|
||||
message: z.string().describe('Description of the result'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user