@dcluttr/dclare-mcp 0.1.5 → 0.1.6

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,12 +17,19 @@ function buildJoinedCubeMap(dataset, allDatasets) {
17
17
  }
18
18
  return map;
19
19
  }
20
- function validateMember(member, allowedLocal, joinedCubes, label) {
20
+ function validateMember(member, allowedLocal, joinedCubes, label, currentCubeName) {
21
21
  if (isJoinedMember(member)) {
22
22
  const dotIndex = member.indexOf(".");
23
23
  const cubeName = member.slice(0, dotIndex);
24
24
  const columnName = member.slice(dotIndex + 1);
25
25
  const stripped = normalizeMemberName(columnName);
26
+ // If the prefix matches the current cube, treat as a local member
27
+ if (currentCubeName && cubeName === currentCubeName) {
28
+ if (allowedLocal.has(stripped)) {
29
+ return stripped;
30
+ }
31
+ throw new AppError("INVALID_QUERY", `${label} '${member}' is not allowed for this dataset.`, 400);
32
+ }
26
33
  const joinedColumns = joinedCubes.get(cubeName);
27
34
  if (joinedColumns && joinedColumns.has(stripped)) {
28
35
  return `${cubeName}.${stripped}`;
@@ -41,7 +48,7 @@ export class QueryGuardrails {
41
48
  const allowedMetrics = new Set(dataset.metrics.map((metric) => metric.name));
42
49
  const allowedSegments = new Set((dataset.segments ?? []).map((segment) => segment.name));
43
50
  const joinedCubes = buildJoinedCubeMap(dataset, allDatasets ?? []);
44
- const validatedDimensions = (input.dimensions ?? []).map((d) => validateMember(d, allowedColumns, joinedCubes, "Dimension"));
51
+ const validatedDimensions = (input.dimensions ?? []).map((d) => validateMember(d, allowedColumns, joinedCubes, "Dimension", dataset.cubeName));
45
52
  for (const metric of input.metrics ?? []) {
46
53
  const normalized = normalizeMemberName(metric);
47
54
  if (!allowedMetrics.has(normalized)) {
@@ -50,15 +57,23 @@ export class QueryGuardrails {
50
57
  }
51
58
  for (const segment of input.segments ?? []) {
52
59
  const normalized = normalizeMemberName(segment);
60
+ // Strip prefix if it matches current cube name
61
+ if (isJoinedMember(segment)) {
62
+ const dotIndex = segment.indexOf(".");
63
+ const cubeName = segment.slice(0, dotIndex);
64
+ if (cubeName !== dataset.cubeName) {
65
+ throw new AppError("INVALID_QUERY", `Segment '${segment}' is not allowed for this dataset.`, 400);
66
+ }
67
+ }
53
68
  if (!allowedSegments.has(normalized)) {
54
69
  throw new AppError("INVALID_QUERY", `Segment '${segment}' is not allowed for this dataset.`, 400);
55
70
  }
56
71
  }
57
72
  const validatedTimeDimensions = (input.timeDimensions ?? []).map((item) => {
58
- const resolved = validateMember(item.member, allowedColumns, joinedCubes, "Time dimension");
73
+ const resolved = validateMember(item.member, allowedColumns, joinedCubes, "Time dimension", dataset.cubeName);
59
74
  return { ...item, member: resolved };
60
75
  });
61
- const validFilters = this.validateFilters(input.filters ?? [], allowedColumns, joinedCubes);
76
+ const validFilters = this.validateFilters(input.filters ?? [], allowedColumns, joinedCubes, dataset.cubeName);
62
77
  const allFilters = [...validFilters];
63
78
  const order = this.validateOrder(input.order ?? [], allowedColumns, allowedMetrics, joinedCubes, dataset.cubeName);
64
79
  const normalizedSegments = (input.segments ?? []).map((segment) => normalizeMemberName(segment));
@@ -81,9 +96,9 @@ export class QueryGuardrails {
81
96
  limit
82
97
  };
83
98
  }
84
- validateFilters(filters, allowedColumns, joinedCubes) {
99
+ validateFilters(filters, allowedColumns, joinedCubes, currentCubeName) {
85
100
  return filters.map((filter) => {
86
- const resolved = validateMember(filter.member, allowedColumns, joinedCubes, "Filter member");
101
+ const resolved = validateMember(filter.member, allowedColumns, joinedCubes, "Filter member", currentCubeName);
87
102
  if (!Array.isArray(filter.values) || filter.values.length === 0) {
88
103
  throw new AppError("INVALID_QUERY", `Filter '${filter.member}' must include at least one value.`, 400);
89
104
  }
@@ -98,7 +113,7 @@ export class QueryGuardrails {
98
113
  for (const item of order) {
99
114
  let member;
100
115
  try {
101
- member = validateMember(item.member, allowedColumns, joinedCubes, "Order member");
116
+ member = validateMember(item.member, allowedColumns, joinedCubes, "Order member", cubeName);
102
117
  }
103
118
  catch {
104
119
  const normalized = normalizeMemberName(item.member);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcluttr/dclare-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "MCP server for secure talk-to-data on Cube + ClickHouse",
6
6
  "bin": {