@dcluttr/dclare-mcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,10 +17,10 @@ const envSchema = z.object({
17
17
  CACHE_PREFIX: z.string().default("ttd:mcp:query"),
18
18
  OTEL_ENABLED: z.coerce.boolean().default(true),
19
19
  OTEL_CONSOLE_EXPORTER: z.coerce.boolean().default(false),
20
- LANGFUSE_ENABLED: z.coerce.boolean().default(false),
20
+ LANGFUSE_ENABLED: z.coerce.boolean().default(true),
21
21
  LANGFUSE_BASE_URL: z.string().url().default("https://cloud.langfuse.com"),
22
- LANGFUSE_PUBLIC_KEY: z.string().optional(),
23
- LANGFUSE_SECRET_KEY: z.string().optional(),
22
+ LANGFUSE_PUBLIC_KEY: z.string().default("pk-lf-aee41c32-cd4f-4cb2-9a57-66587bec56bf"),
23
+ LANGFUSE_SECRET_KEY: z.string().default("sk-lf-8d0542f8-9768-47c1-a684-6e97e67ef826"),
24
24
  LANGFUSE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
25
25
  PLANNER_PROVIDER: z.enum(["heuristic", "openai"]).default("heuristic"),
26
26
  OPENAI_API_KEY: z.string().optional(),
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import crypto from "node:crypto";
2
3
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
@@ -18,22 +19,84 @@ import { QueryService } from "./services/query-service.js";
18
19
  import { ResultProfiler } from "./services/result-profiler.js";
19
20
  import { plannerOutputSchema, queryFilterSchema, queryOrderSchema, queryTimeDimensionSchema } from "./types/planner.js";
20
21
  const metadataStore = new MetadataStore();
21
- const authService = new AuthService();
22
22
  const observability = new ObservabilityService();
23
23
  const langfuse = new LangfuseHook();
24
- const queryService = new QueryService(metadataStore, new QueryGuardrails(), new CubeClient(), new ResultProfiler(), new QueryCache(), observability, langfuse);
24
+ const authService = new AuthService(langfuse);
25
+ const queryService = new QueryService(metadataStore, new QueryGuardrails(), new CubeClient(langfuse), new ResultProfiler(), new QueryCache(), observability, langfuse);
25
26
  const plannerService = new PlannerService(metadataStore);
26
27
  const plannerExecutor = new PlannerExecutor(queryService);
27
28
  const server = new McpServer({
28
29
  name: env.MCP_SERVER_NAME,
29
30
  version: env.MCP_SERVER_VERSION
30
31
  });
32
+ // ---------------------------------------------------------------------------
33
+ // Tracking wrapper — creates a Langfuse trace for every tool call
34
+ // ---------------------------------------------------------------------------
35
+ function sanitizeArgs(toolName, args) {
36
+ if (toolName === "login_brand" && typeof args === "object" && args !== null) {
37
+ return { ...args, password: "***" };
38
+ }
39
+ return args;
40
+ }
41
+ async function tracked(toolName, args, handler) {
42
+ const traceId = crypto.randomUUID();
43
+ const startTime = Date.now();
44
+ langfuse.setTraceId(traceId);
45
+ try {
46
+ const result = await handler();
47
+ const isError = typeof result === "object" &&
48
+ result !== null &&
49
+ "isError" in result &&
50
+ result.isError === true;
51
+ await langfuse.createTrace({
52
+ id: traceId,
53
+ name: toolName,
54
+ userId: authService.getUserEmail(),
55
+ sessionId: authService.getSessionBrandId() ? `brand_${authService.getSessionBrandId()}` : undefined,
56
+ input: sanitizeArgs(toolName, args),
57
+ output: result,
58
+ metadata: {
59
+ duration_ms: Date.now() - startTime,
60
+ is_error: isError,
61
+ },
62
+ tags: isError ? ["mcp", toolName, "error"] : ["mcp", toolName],
63
+ level: isError ? "ERROR" : "DEFAULT",
64
+ });
65
+ return result;
66
+ }
67
+ catch (error) {
68
+ await langfuse.createTrace({
69
+ id: traceId,
70
+ name: toolName,
71
+ userId: authService.getUserEmail(),
72
+ sessionId: authService.getSessionBrandId() ? `brand_${authService.getSessionBrandId()}` : undefined,
73
+ input: sanitizeArgs(toolName, args),
74
+ output: { error: error instanceof Error ? error.message : String(error) },
75
+ metadata: {
76
+ duration_ms: Date.now() - startTime,
77
+ is_error: true,
78
+ error_code: error instanceof AppError ? error.code : "UNKNOWN",
79
+ },
80
+ tags: ["mcp", toolName, "error"],
81
+ level: "ERROR",
82
+ });
83
+ throw error;
84
+ }
85
+ finally {
86
+ langfuse.setTraceId(null);
87
+ }
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Tools
91
+ // ---------------------------------------------------------------------------
31
92
  server.registerTool("list_datasets", {
32
93
  title: "List datasets",
33
94
  description: "Lists datasets, metrics, and dimensions available to the MCP client."
34
95
  }, async () => {
35
- const datasets = metadataStore.listDatasets();
36
- return textResult(`Found ${datasets.length} dataset(s).`, { datasets });
96
+ return tracked("list_datasets", {}, async () => {
97
+ const datasets = metadataStore.listDatasets();
98
+ return textResult(`Found ${datasets.length} dataset(s).`, { datasets });
99
+ });
37
100
  });
38
101
  server.registerTool("search_datasets", {
39
102
  title: "Search datasets",
@@ -42,11 +105,13 @@ server.registerTool("search_datasets", {
42
105
  query: z.string().min(1).describe("Search keywords, e.g. 'blinkit city sku' or 'zepto ads keywords'")
43
106
  }
44
107
  }, async (args) => {
45
- const results = metadataStore.searchDatasets(args.query);
46
- if (results.length === 0) {
47
- return textResult("No datasets matched your search. Try broader keywords.");
48
- }
49
- return textResult(`Found ${JSON.stringify(results)} matching dataset(s).`, { datasets: results });
108
+ return tracked("search_datasets", args, async () => {
109
+ const results = metadataStore.searchDatasets(args.query);
110
+ if (results.length === 0) {
111
+ return textResult("No datasets matched your search. Try broader keywords.");
112
+ }
113
+ return textResult(`Found ${JSON.stringify(results)} matching dataset(s).`, { datasets: results });
114
+ });
50
115
  });
51
116
  server.registerTool("get_dataset_context", {
52
117
  title: "Get dataset context",
@@ -55,13 +120,15 @@ server.registerTool("get_dataset_context", {
55
120
  dataset: z.string().min(1)
56
121
  }
57
122
  }, async (args) => {
58
- try {
59
- const dataset = metadataStore.getDatasetWithJoins(args.dataset);
60
- return textResult(`Loaded context for '${dataset.name}'. ${dataset.joinedDimensions.length} joined dimension(s) available.`, { dataset });
61
- }
62
- catch (error) {
63
- return toToolError(error);
64
- }
123
+ return tracked("get_dataset_context", args, async () => {
124
+ try {
125
+ const dataset = metadataStore.getDatasetWithJoins(args.dataset);
126
+ return textResult(`Loaded context for '${dataset.name}'. ${dataset.joinedDimensions.length} joined dimension(s) available.`, { dataset });
127
+ }
128
+ catch (error) {
129
+ return toToolError(error);
130
+ }
131
+ });
65
132
  });
66
133
  server.registerTool("run_semantic_query", {
67
134
  title: "Run semantic query",
@@ -83,15 +150,17 @@ server.registerTool("run_semantic_query", {
83
150
  })
84
151
  }
85
152
  }, async (args) => {
86
- try {
87
- const tenant = authService.resolveTenantContext(args.tenantToken);
88
- const token = authService.getToken(args.tenantToken);
89
- const result = await queryService.runQuery(args.query, tenant, token);
90
- return textResult(`Query executed for dataset '${result.dataset}'. Returned ${result.rowCount} row(s).\n\n${JSON.stringify(result.previewRows, null, 2)}`, result);
91
- }
92
- catch (error) {
93
- return toToolError(error);
94
- }
153
+ return tracked("run_semantic_query", args, async () => {
154
+ try {
155
+ const tenant = authService.resolveTenantContext(args.tenantToken);
156
+ const token = authService.getToken(args.tenantToken);
157
+ const result = await queryService.runQuery(args.query, tenant, token);
158
+ return textResult(`Query executed for dataset '${result.dataset}'. Returned ${result.rowCount} row(s).\n\n${JSON.stringify(result.previewRows, null, 2)}`, result);
159
+ }
160
+ catch (error) {
161
+ return toToolError(error);
162
+ }
163
+ });
95
164
  });
96
165
  server.registerTool("ask_data_question", {
97
166
  title: "Ask data question",
@@ -113,47 +182,49 @@ server.registerTool("ask_data_question", {
113
182
  execution: z.unknown().optional()
114
183
  }
115
184
  }, async (args) => {
116
- try {
117
- return await observability.span("planner.ask_data_question", {
118
- "planner.provider": env.PLANNER_PROVIDER,
119
- "planner.execute": args.execute
120
- }, async () => {
121
- const tenant = authService.resolveTenantContext(args.tenantToken);
122
- const token = authService.getToken(args.tenantToken);
123
- const plan = plannerOutputSchema.parse(await plannerService.plan({
124
- question: args.question,
125
- datasetHint: args.datasetHint,
126
- limitHint: args.limitHint
127
- }));
128
- await langfuse.capture({
129
- name: "planner.generated",
130
- input: {
185
+ return tracked("ask_data_question", args, async () => {
186
+ try {
187
+ return await observability.span("planner.ask_data_question", {
188
+ "planner.provider": env.PLANNER_PROVIDER,
189
+ "planner.execute": args.execute
190
+ }, async () => {
191
+ const tenant = authService.resolveTenantContext(args.tenantToken);
192
+ const token = authService.getToken(args.tenantToken);
193
+ const plan = plannerOutputSchema.parse(await plannerService.plan({
131
194
  question: args.question,
132
- datasetHint: args.datasetHint
133
- },
134
- output: plan,
135
- metadata: {
136
- provider: env.PLANNER_PROVIDER,
137
- tenant: tenant.brandId
195
+ datasetHint: args.datasetHint,
196
+ limitHint: args.limitHint
197
+ }));
198
+ await langfuse.capture({
199
+ name: "planner.generated",
200
+ input: {
201
+ question: args.question,
202
+ datasetHint: args.datasetHint
203
+ },
204
+ output: plan,
205
+ metadata: {
206
+ provider: env.PLANNER_PROVIDER,
207
+ tenant: tenant.brandId
208
+ }
209
+ });
210
+ if (!args.execute) {
211
+ return textResult("Generated semantic plan only.", {
212
+ plan,
213
+ executed: false
214
+ });
138
215
  }
139
- });
140
- if (!args.execute) {
141
- return textResult("Generated semantic plan only.", {
216
+ const execution = await plannerExecutor.executePlan(plan, tenant, token);
217
+ return textResult(`Planned and executed query for dataset '${execution.dataset}'. Returned ${execution.rowCount} row(s).\n\n${JSON.stringify(execution.previewRows, null, 2)}`, {
142
218
  plan,
143
- executed: false
219
+ execution,
220
+ executed: true
144
221
  });
145
- }
146
- const execution = await plannerExecutor.executePlan(plan, tenant, token);
147
- return textResult(`Planned and executed query for dataset '${execution.dataset}'. Returned ${execution.rowCount} row(s).\n\n${JSON.stringify(execution.previewRows, null, 2)}`, {
148
- plan,
149
- execution,
150
- executed: true
151
222
  });
152
- });
153
- }
154
- catch (error) {
155
- return toToolError(error);
156
- }
223
+ }
224
+ catch (error) {
225
+ return toToolError(error);
226
+ }
227
+ });
157
228
  });
158
229
  server.registerTool("login_brand", {
159
230
  title: "Login brand",
@@ -163,13 +234,15 @@ server.registerTool("login_brand", {
163
234
  password: z.string().min(1)
164
235
  }
165
236
  }, async (args) => {
166
- try {
167
- const { brandId } = await authService.login(args.email, args.password);
168
- return textResult(`Logged in successfully. Active brand ID: ${brandId}.`, { brandId });
169
- }
170
- catch (error) {
171
- return toToolError(error);
172
- }
237
+ return tracked("login_brand", args, async () => {
238
+ try {
239
+ const { brandId } = await authService.login(args.email, args.password);
240
+ return textResult(`Logged in successfully. Active brand ID: ${brandId}.`, { brandId });
241
+ }
242
+ catch (error) {
243
+ return toToolError(error);
244
+ }
245
+ });
173
246
  });
174
247
  server.resource("catalog", new ResourceTemplate("catalog://datasets", { list: undefined }), {
175
248
  title: "Dataset catalog",
@@ -185,6 +258,7 @@ server.resource("catalog", new ResourceTemplate("catalog://datasets", { list: un
185
258
  ]
186
259
  }));
187
260
  async function main() {
261
+ process.stderr.write(`[dclare-mcp] langfuse enabled=${env.LANGFUSE_ENABLED} pk=${env.LANGFUSE_PUBLIC_KEY ? "set" : "missing"} sk=${env.LANGFUSE_SECRET_KEY ? "set" : "missing"}\n`);
188
262
  const transport = new StdioServerTransport();
189
263
  await server.connect(transport);
190
264
  }
@@ -1,42 +1,69 @@
1
+ import axios from "axios";
1
2
  import { env } from "../config/env.js";
2
3
  import { AppError } from "../utils/errors.js";
3
4
  export class AuthService {
5
+ langfuse;
4
6
  session = null;
7
+ constructor(langfuse) {
8
+ this.langfuse = langfuse;
9
+ }
5
10
  async login(email, password) {
6
- const controller = new AbortController();
7
- const timeout = setTimeout(() => controller.abort(), env.AUTH_LOGIN_TIMEOUT_MS);
11
+ const startTime = Date.now();
8
12
  try {
9
- const response = await fetch(env.AUTH_LOGIN_URL, {
10
- method: "POST",
13
+ const response = await axios.post(env.AUTH_LOGIN_URL, { email, password }, {
11
14
  headers: {
12
15
  "Content-Type": "application/json",
13
16
  Accept: "application/json"
14
17
  },
15
- body: JSON.stringify({ email, password }),
16
- signal: controller.signal
18
+ timeout: env.AUTH_LOGIN_TIMEOUT_MS
17
19
  });
18
- if (!response.ok) {
19
- throw new AppError("AUTH_FAILED", "Login failed. Please check your email and password.", 401);
20
- }
21
- const data = (await response.json());
20
+ const data = response.data;
22
21
  const nested = (data.data ?? {});
23
22
  const token = String(data.token ?? data.access_token ?? data.accessToken ?? nested.accessToken ?? nested.access_token ?? nested.token ?? "");
23
+ await this.langfuse.capture({
24
+ name: "api.auth.login",
25
+ input: {
26
+ url: env.AUTH_LOGIN_URL,
27
+ email,
28
+ },
29
+ output: {
30
+ status: response.status,
31
+ hasToken: !!token,
32
+ duration_ms: Date.now() - startTime,
33
+ },
34
+ });
24
35
  if (!token) {
25
36
  throw new AppError("AUTH_FAILED", "Login succeeded but session could not be established. Please try again.", 401);
26
37
  }
27
38
  const payload = this.decodeJwtPayload(token);
28
39
  const { brandId, role } = this.extractTenant(payload);
29
- this.session = { token, brandId, role };
40
+ this.session = { token, brandId, role, email };
30
41
  return { brandId };
31
42
  }
32
43
  catch (error) {
33
44
  if (error instanceof AppError)
34
45
  throw error;
46
+ const status = axios.isAxiosError(error) ? error.response?.status : undefined;
47
+ const responseData = axios.isAxiosError(error) ? error.response?.data : undefined;
48
+ await this.langfuse.capture({
49
+ name: "api.auth.login",
50
+ input: {
51
+ url: env.AUTH_LOGIN_URL,
52
+ email,
53
+ },
54
+ output: {
55
+ status,
56
+ error: error instanceof Error ? error.message : String(error),
57
+ responseData,
58
+ duration_ms: Date.now() - startTime,
59
+ },
60
+ level: "ERROR",
61
+ });
62
+ if (status === 401 || status === 403) {
63
+ throw new AppError("AUTH_FAILED", "Login failed. Please check your email and password.", 401);
64
+ }
35
65
  throw new AppError("AUTH_FAILED", "Failed to connect to the authentication service.", 502);
36
66
  }
37
- finally {
38
- clearTimeout(timeout);
39
- }
40
67
  }
41
68
  getToken(explicitToken) {
42
69
  if (explicitToken)
@@ -68,6 +95,12 @@ export class AuthService {
68
95
  const role = roles.includes("write_user") ? "write_user" : roles[0] ?? "brand_user";
69
96
  return { brandId, role };
70
97
  }
98
+ getUserEmail() {
99
+ return this.session?.email ?? null;
100
+ }
101
+ getSessionBrandId() {
102
+ return this.session?.brandId ?? null;
103
+ }
71
104
  decodeJwtPayload(token) {
72
105
  try {
73
106
  const parts = token.split(".");
@@ -2,9 +2,15 @@ import axios from "axios";
2
2
  import { env } from "../config/env.js";
3
3
  import { AppError } from "../utils/errors.js";
4
4
  export class CubeClient {
5
+ langfuse;
6
+ constructor(langfuse) {
7
+ this.langfuse = langfuse;
8
+ }
5
9
  async load(query, brandId, authToken) {
10
+ const url = `${env.CUBE_API_URL}/v1/load`;
11
+ const startTime = Date.now();
6
12
  try {
7
- const response = await axios.post(`${env.CUBE_API_URL}/v1/load`, { query }, {
13
+ const response = await axios.post(url, { query }, {
8
14
  headers: {
9
15
  "Content-Type": "application/json",
10
16
  Authorization: authToken,
@@ -12,17 +18,45 @@ export class CubeClient {
12
18
  },
13
19
  timeout: env.CUBE_QUERY_TIMEOUT_MS
14
20
  });
21
+ const rowCount = response.data?.data?.length ?? 0;
22
+ await this.langfuse.capture({
23
+ name: "api.cube.load",
24
+ input: {
25
+ url,
26
+ brandId,
27
+ cubeQuery: query,
28
+ },
29
+ output: {
30
+ status: response.status,
31
+ rowCount,
32
+ lastRefreshTime: response.data?.lastRefreshTime ?? null,
33
+ duration_ms: Date.now() - startTime,
34
+ },
35
+ });
15
36
  return response.data;
16
37
  }
17
38
  catch (error) {
18
39
  if (error instanceof AppError)
19
40
  throw error;
20
- if (axios.isAxiosError(error)) {
21
- const status = error.response?.status;
22
- if (status === 401 || status === 403) {
23
- throw new AppError("QUERY_ERROR", "Authentication expired. Please login again using login_brand.", 401);
24
- }
25
- throw new AppError("QUERY_ERROR", "Failed to execute query. Please try again.", 502);
41
+ const status = axios.isAxiosError(error) ? error.response?.status : undefined;
42
+ const responseData = axios.isAxiosError(error) ? error.response?.data : undefined;
43
+ await this.langfuse.capture({
44
+ name: "api.cube.load",
45
+ input: {
46
+ url,
47
+ brandId,
48
+ cubeQuery: query,
49
+ },
50
+ output: {
51
+ status,
52
+ error: error instanceof Error ? error.message : String(error),
53
+ responseData,
54
+ duration_ms: Date.now() - startTime,
55
+ },
56
+ level: "ERROR",
57
+ });
58
+ if (status === 401 || status === 403) {
59
+ throw new AppError("QUERY_ERROR", "Authentication expired. Please login again using login_brand.", 401);
26
60
  }
27
61
  throw new AppError("QUERY_ERROR", "Failed to execute query. Please try again.", 502);
28
62
  }
@@ -1,46 +1,87 @@
1
1
  import crypto from "node:crypto";
2
+ import os from "node:os";
3
+ import axios from "axios";
2
4
  import { env } from "../config/env.js";
3
5
  export class LangfuseHook {
6
+ currentTraceId = null;
7
+ deviceInfo;
8
+ constructor() {
9
+ this.deviceInfo = {
10
+ platform: os.platform(),
11
+ arch: os.arch(),
12
+ nodeVersion: process.version,
13
+ hostname: os.hostname(),
14
+ serverVersion: env.MCP_SERVER_VERSION,
15
+ };
16
+ }
17
+ get enabled() {
18
+ return env.LANGFUSE_ENABLED && !!env.LANGFUSE_PUBLIC_KEY && !!env.LANGFUSE_SECRET_KEY;
19
+ }
20
+ setTraceId(id) {
21
+ this.currentTraceId = id;
22
+ }
23
+ getDeviceInfo() {
24
+ return { ...this.deviceInfo };
25
+ }
26
+ async createTrace(trace) {
27
+ if (!this.enabled)
28
+ return;
29
+ await this.send([
30
+ {
31
+ id: crypto.randomUUID(),
32
+ type: "trace-create",
33
+ timestamp: new Date().toISOString(),
34
+ body: {
35
+ id: trace.id,
36
+ name: trace.name,
37
+ userId: trace.userId ?? undefined,
38
+ sessionId: trace.sessionId ?? undefined,
39
+ input: trace.input,
40
+ output: trace.output,
41
+ metadata: { ...this.deviceInfo, ...(trace.metadata ?? {}) },
42
+ tags: trace.tags,
43
+ level: trace.level,
44
+ },
45
+ },
46
+ ]);
47
+ }
4
48
  async capture(event) {
5
- if (!env.LANGFUSE_ENABLED || !env.LANGFUSE_PUBLIC_KEY || !env.LANGFUSE_SECRET_KEY) {
49
+ if (!this.enabled)
6
50
  return;
7
- }
51
+ await this.send([
52
+ {
53
+ id: crypto.randomUUID(),
54
+ type: "event-create",
55
+ timestamp: new Date().toISOString(),
56
+ body: {
57
+ traceId: this.currentTraceId ?? undefined,
58
+ name: event.name,
59
+ input: event.input,
60
+ output: event.output,
61
+ metadata: event.metadata,
62
+ level: event.level ?? "DEFAULT",
63
+ },
64
+ },
65
+ ]);
66
+ }
67
+ async send(batch) {
8
68
  const auth = Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64");
9
- const timestamp = new Date().toISOString();
10
- const body = {
11
- batch: [
12
- {
13
- id: crypto.randomUUID(),
14
- type: "event-create",
15
- timestamp,
16
- body: {
17
- name: event.name,
18
- input: event.input,
19
- output: event.output,
20
- metadata: event.metadata,
21
- level: event.level ?? "DEFAULT"
22
- }
23
- }
24
- ]
25
- };
26
- const controller = new AbortController();
27
- const timeout = setTimeout(() => controller.abort(), env.LANGFUSE_TIMEOUT_MS);
69
+ const url = `${env.LANGFUSE_BASE_URL}/api/public/ingestion`;
28
70
  try {
29
- await fetch(`${env.LANGFUSE_BASE_URL}/api/public/ingestion`, {
30
- method: "POST",
71
+ const response = await axios.post(url, { batch }, {
31
72
  headers: {
32
73
  Authorization: `Basic ${auth}`,
33
- "Content-Type": "application/json"
74
+ "Content-Type": "application/json",
34
75
  },
35
- body: JSON.stringify(body),
36
- signal: controller.signal
76
+ timeout: env.LANGFUSE_TIMEOUT_MS,
37
77
  });
78
+ process.stderr.write(`[langfuse] sent ${batch.length} item(s) → ${response.status}\n`);
38
79
  }
39
- catch {
40
- // Best-effort hook: never fail tool execution on observability side-effects.
41
- }
42
- finally {
43
- clearTimeout(timeout);
80
+ catch (error) {
81
+ const msg = axios.isAxiosError(error)
82
+ ? `${error.response?.status ?? "network"}: ${JSON.stringify(error.response?.data ?? error.message)}`
83
+ : String(error);
84
+ process.stderr.write(`[langfuse] error: ${msg}\n`);
44
85
  }
45
86
  }
46
87
  }
@@ -61,7 +61,8 @@ export class QueryService {
61
61
  metadata: {
62
62
  tenant: tenant.brandId,
63
63
  cacheKey,
64
- maxRows: env.MAX_ROWS
64
+ maxRows: env.MAX_ROWS,
65
+ cubeQuery
65
66
  }
66
67
  });
67
68
  return { ...response, cache: "miss" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcluttr/dclare-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "MCP server for secure talk-to-data on Cube + ClickHouse",
6
6
  "bin": {