mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-25 08:48:08 +02:00
feat: trim MCP query response payloads
This commit is contained in:
parent
8ebc4ce107
commit
133a2f700a
7 changed files with 235 additions and 1703 deletions
|
|
@ -65,7 +65,7 @@
|
|||
},
|
||||
"limit": {
|
||||
"default": 10,
|
||||
"description": "Maximum wiki pages to return. Defaults to 10.",
|
||||
"description": "Maximum wiki pages to return.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50
|
||||
|
|
@ -307,7 +307,7 @@
|
|||
{
|
||||
"name": "sl_query",
|
||||
"title": "Semantic Layer Query",
|
||||
"description": "Execute a semantic-layer query and return rows, headers, generated SQL, and plan details. Example: sl_query({ connectionId: \"warehouse\", measures: [\"orders.order_count\"], dimensions: [{ field: \"orders.created_at\", granularity: \"month\" }] }).",
|
||||
"description": "Execute a semantic-layer query and return headers, rows, and total row count, plus correctness notes (e.g. compile-only or fan-out) when relevant. The generated SQL and full query plan are omitted by default; request them with include: [\"sql\"] and/or include: [\"plan\"]. Example: sl_query({ connectionId: \"warehouse\", measures: [\"orders.order_count\"], dimensions: [{ field: \"orders.created_at\", granularity: \"month\" }], include: [\"sql\"] }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -403,7 +403,7 @@
|
|||
},
|
||||
"direction": {
|
||||
"default": "asc",
|
||||
"description": "Sort direction: \"asc\" or \"desc\". Defaults to \"asc\".",
|
||||
"description": "Sort direction for this field.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"asc",
|
||||
|
|
@ -418,15 +418,27 @@
|
|||
},
|
||||
"limit": {
|
||||
"default": 1000,
|
||||
"description": "Maximum rows to return. Defaults to 1000.",
|
||||
"description": "Maximum rows to return.",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"include_empty": {
|
||||
"default": true,
|
||||
"description": "Whether to include empty dimension groups. Defaults to true.",
|
||||
"description": "Whether to include empty dimension groups.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"include": {
|
||||
"default": [],
|
||||
"description": "Extra detail to attach to the response: \"sql\" for the generated SQL, \"plan\" for the full query plan.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"plan",
|
||||
"sql"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -443,9 +455,6 @@
|
|||
"dialect": {
|
||||
"type": "string"
|
||||
},
|
||||
"sql": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -462,6 +471,15 @@
|
|||
"totalRows": {
|
||||
"type": "number"
|
||||
},
|
||||
"notes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sql": {
|
||||
"type": "string"
|
||||
},
|
||||
"plan": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
|
|
@ -471,7 +489,6 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"sql",
|
||||
"headers",
|
||||
"rows",
|
||||
"totalRows"
|
||||
|
|
@ -1241,8 +1258,8 @@
|
|||
}
|
||||
},
|
||||
"limit": {
|
||||
"description": "Maximum refs to return. Defaults to 15.",
|
||||
"default": 15,
|
||||
"description": "Maximum refs to return.",
|
||||
"default": 10,
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50
|
||||
|
|
@ -1396,7 +1413,7 @@
|
|||
"description": "Parser-validated read-only SQL, e.g. \"select count(*) from public.orders\"."
|
||||
},
|
||||
"maxRows": {
|
||||
"description": "Maximum rows to return. Defaults to 1000.",
|
||||
"description": "Maximum rows to return.",
|
||||
"default": 1000,
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
|
|
|
|||
|
|
@ -307,16 +307,12 @@ describe('createKtxMcpServer', () => {
|
|||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
headers: ['status', 'count'],
|
||||
headerTypes: ['text', 'bigint'],
|
||||
rows: [['paid', 42]],
|
||||
rowCount: 1,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
text: JSON.stringify({
|
||||
headers: ['status', 'count'],
|
||||
headerTypes: ['text', 'bigint'],
|
||||
rows: [['paid', 42]],
|
||||
rowCount: 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
|
|
@ -598,6 +594,92 @@ describe('createKtxMcpServer', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('sl_query default response omits plan and sql but keeps compile-only and fan-out notes', async () => {
|
||||
const fake = makeFakeServer();
|
||||
const semanticLayer: KtxSemanticLayerMcpPort = {
|
||||
readSource: vi.fn(),
|
||||
query: vi.fn<KtxSemanticLayerMcpPort['query']>().mockResolvedValue({
|
||||
connectionId: 'warehouse',
|
||||
dialect: 'postgres',
|
||||
sql: 'select count(*) from public.orders',
|
||||
headers: ['order_count'],
|
||||
rows: [],
|
||||
totalRows: 0,
|
||||
plan: {
|
||||
sources_used: ['orders'],
|
||||
has_fan_out: true,
|
||||
fan_out_description: 'orders fans out across line_items',
|
||||
execution: { mode: 'compile_only', reason: 'No execution adapter configured.' },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
createKtxMcpServer({
|
||||
server: fake.server,
|
||||
userContext: { userId: 'local-user' },
|
||||
contextTools: { semanticLayer },
|
||||
});
|
||||
|
||||
const result = await getTool(fake.tools, 'sl_query').handler({
|
||||
connectionId: 'warehouse',
|
||||
measures: ['orders.order_count'],
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
structuredContent: {
|
||||
connectionId: 'warehouse',
|
||||
dialect: 'postgres',
|
||||
headers: ['order_count'],
|
||||
rows: [],
|
||||
totalRows: 0,
|
||||
notes: ['No execution adapter configured.', 'orders fans out across line_items'],
|
||||
},
|
||||
});
|
||||
const structured = (result as { structuredContent: Record<string, unknown> }).structuredContent;
|
||||
expect(structured.sql).toBeUndefined();
|
||||
expect(structured.plan).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sl_query attaches sql and plan only when include requests them', async () => {
|
||||
const fake = makeFakeServer();
|
||||
const plan = { sources_used: ['orders'], execution: { mode: 'executed' } };
|
||||
const semanticLayer: KtxSemanticLayerMcpPort = {
|
||||
readSource: vi.fn(),
|
||||
query: vi.fn<KtxSemanticLayerMcpPort['query']>().mockResolvedValue({
|
||||
connectionId: 'warehouse',
|
||||
dialect: 'postgres',
|
||||
sql: 'select count(*) from public.orders',
|
||||
headers: ['order_count'],
|
||||
rows: [[3]],
|
||||
totalRows: 1,
|
||||
plan,
|
||||
}),
|
||||
};
|
||||
|
||||
createKtxMcpServer({
|
||||
server: fake.server,
|
||||
userContext: { userId: 'local-user' },
|
||||
contextTools: { semanticLayer },
|
||||
});
|
||||
|
||||
const result = await getTool(fake.tools, 'sl_query').handler({
|
||||
connectionId: 'warehouse',
|
||||
measures: ['orders.order_count'],
|
||||
include: ['plan', 'sql'],
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
structuredContent: {
|
||||
sql: 'select count(*) from public.orders',
|
||||
plan,
|
||||
rows: [[3]],
|
||||
totalRows: 1,
|
||||
},
|
||||
});
|
||||
const structured = (result as { structuredContent: Record<string, unknown> }).structuredContent;
|
||||
expect(structured.notes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('entity_details rejects sql-style schema table ref aliases', async () => {
|
||||
const fake = makeFakeServer();
|
||||
const entityDetails = makeAllContextTools().entityDetails!;
|
||||
|
|
@ -798,7 +880,7 @@ describe('createKtxMcpServer', () => {
|
|||
connectionId: '00000000-0000-4000-8000-000000000001',
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
content: [{ type: 'text', text: JSON.stringify({ runId: 'run-1' }, null, 2) }],
|
||||
content: [{ type: 'text', text: JSON.stringify({ runId: 'run-1' }) }],
|
||||
structuredContent: { runId: 'run-1' },
|
||||
});
|
||||
expect(ingest.ingest).toHaveBeenCalledWith({
|
||||
|
|
@ -825,21 +907,17 @@ describe('createKtxMcpServer', () => {
|
|||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
runId: 'run-1',
|
||||
status: 'done',
|
||||
stage: 'done',
|
||||
done: true,
|
||||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
text: JSON.stringify({
|
||||
runId: 'run-1',
|
||||
status: 'done',
|
||||
stage: 'done',
|
||||
done: true,
|
||||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
|
|
@ -1047,19 +1125,15 @@ describe('createKtxMcpServer', () => {
|
|||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
connections: [
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Warehouse',
|
||||
connectionType: 'POSTGRES',
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
text: JSON.stringify({
|
||||
connections: [
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Warehouse',
|
||||
connectionType: 'POSTGRES',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue