@owox/backend 0.30.0 → 0.30.1

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.
Files changed (46) hide show
  1. package/dist/src/data-marts/dto/domain/list-data-marts.command.d.ts +3 -1
  2. package/dist/src/data-marts/dto/domain/list-data-marts.command.js +3 -1
  3. package/dist/src/data-marts/facades/mcp-data-marts.facade.d.ts +13 -0
  4. package/dist/src/data-marts/facades/mcp-data-marts.facade.impl.d.ts +2 -1
  5. package/dist/src/data-marts/facades/mcp-data-marts.facade.impl.js +31 -7
  6. package/dist/src/data-marts/services/blended-field-display-name.d.ts +7 -0
  7. package/dist/src/data-marts/services/blended-field-display-name.js +8 -0
  8. package/dist/src/data-marts/services/blended-report-data.service.js +2 -1
  9. package/dist/src/data-marts/services/data-mart.service.d.ts +2 -0
  10. package/dist/src/data-marts/services/data-mart.service.js +3 -0
  11. package/dist/src/data-marts/services/report-sql-composer.service.js +2 -1
  12. package/dist/src/data-marts/use-cases/list-data-marts.service.js +1 -0
  13. package/dist/src/data-marts/use-cases/query-data-mart.service.js +13 -2
  14. package/dist/src/ee/mcp/instructions/mcp-system-instructions.d.ts +1 -1
  15. package/dist/src/ee/mcp/instructions/mcp-system-instructions.js +4 -2
  16. package/dist/src/ee/mcp/tools/add-destination.tool.d.ts +1 -0
  17. package/dist/src/ee/mcp/tools/add-destination.tool.js +7 -0
  18. package/dist/src/ee/mcp/tools/create-report-run-schedule.tool.d.ts +4 -1
  19. package/dist/src/ee/mcp/tools/create-report-run-schedule.tool.js +9 -2
  20. package/dist/src/ee/mcp/tools/data-mart-catalog.tool.d.ts +24 -4
  21. package/dist/src/ee/mcp/tools/data-mart-catalog.tool.js +29 -11
  22. package/dist/src/ee/mcp/tools/data-mart-details.tool.d.ts +16 -2
  23. package/dist/src/ee/mcp/tools/data-mart-details.tool.js +25 -4
  24. package/dist/src/ee/mcp/tools/data-mart-ui-path.d.ts +2 -0
  25. package/dist/src/ee/mcp/tools/data-mart-ui-path.js +9 -0
  26. package/dist/src/ee/mcp/tools/delete-report-run-schedule.tool.d.ts +4 -1
  27. package/dist/src/ee/mcp/tools/delete-report-run-schedule.tool.js +9 -2
  28. package/dist/src/ee/mcp/tools/get-data-mart-reports.tool.d.ts +9 -1
  29. package/dist/src/ee/mcp/tools/get-data-mart-reports.tool.js +18 -3
  30. package/dist/src/ee/mcp/tools/list-destinations.tool.d.ts +6 -1
  31. package/dist/src/ee/mcp/tools/list-destinations.tool.js +15 -3
  32. package/dist/src/ee/mcp/tools/list-report-run-schedules.tool.d.ts +16 -1
  33. package/dist/src/ee/mcp/tools/list-report-run-schedules.tool.js +21 -6
  34. package/dist/src/ee/mcp/tools/mcp-project-summary.util.d.ts +7 -0
  35. package/dist/src/ee/mcp/tools/mcp-project-summary.util.js +17 -0
  36. package/dist/src/ee/mcp/tools/query-data-mart.tool.d.ts +65 -2
  37. package/dist/src/ee/mcp/tools/query-data-mart.tool.js +72 -8
  38. package/dist/src/ee/mcp/tools/search-data-marts.tool.d.ts +13 -1
  39. package/dist/src/ee/mcp/tools/search-data-marts.tool.js +21 -11
  40. package/dist/src/ee/mcp/tools/summarize-data-catalog.tool.d.ts +13 -1
  41. package/dist/src/ee/mcp/tools/summarize-data-catalog.tool.js +17 -7
  42. package/dist/src/ee/mcp/tools/tabular-serializer.d.ts +6 -0
  43. package/dist/src/ee/mcp/tools/tabular-serializer.js +32 -3
  44. package/dist/src/ee/mcp/tools/update-report-run-schedule.tool.d.ts +4 -1
  45. package/dist/src/ee/mcp/tools/update-report-run-schedule.tool.js +9 -2
  46. package/package.json +4 -4
@@ -15,19 +15,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.ListReportRunSchedulesTool = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const zod_1 = require("zod");
18
+ const public_origin_service_1 = require("../../../common/config/public-origin.service");
18
19
  const mcp_scheduled_triggers_facade_1 = require("../../../data-marts/facades/mcp-scheduled-triggers.facade");
19
20
  const mcp_tool_definition_1 = require("./mcp-tool.definition");
21
+ const data_mart_ui_path_1 = require("./data-mart-ui-path");
22
+ const mcp_public_url_util_1 = require("./mcp-public-url.util");
20
23
  const inputSchema = zod_1.z.object({}).strict();
21
24
  let ListReportRunSchedulesTool = class ListReportRunSchedulesTool {
22
25
  facade;
26
+ publicOriginService;
23
27
  name = 'list_report_run_schedules';
24
28
  description = 'Lists every scheduled REPORT_RUN trigger (report run schedule) in the current OWOX project that the current MCP user can see, in a single response. Each item includes trigger_id — pass it to update_report_run_schedule to modify that specific schedule or delete_report_run_schedule to remove it — plus the report and data mart it belongs to. To add another schedule for a report, call create_report_run_schedule; creating never replaces existing schedules.';
25
29
  zodSchema = inputSchema.shape;
26
30
  outputSchema = {
27
31
  schedules: zod_1.z.array(zod_1.z.object({
28
32
  trigger_id: zod_1.z.string(),
29
- report: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string() }),
30
- data_mart: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string() }),
33
+ report: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string(), url: zod_1.z.string() }),
34
+ data_mart: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string(), url: zod_1.z.string() }),
35
+ schedules_url: zod_1.z.string().describe('Open report schedules in OWOX.'),
31
36
  cron_expression: zod_1.z.string(),
32
37
  time_zone: zod_1.z.string(),
33
38
  is_active: zod_1.z.boolean(),
@@ -44,8 +49,9 @@ let ListReportRunSchedulesTool = class ListReportRunSchedulesTool {
44
49
  openWorldHint: false,
45
50
  };
46
51
  requiredScopes = ['mcp:read'];
47
- constructor(facade) {
52
+ constructor(facade, publicOriginService) {
48
53
  this.facade = facade;
54
+ this.publicOriginService = publicOriginService;
49
55
  }
50
56
  parseInput(input) {
51
57
  return inputSchema.parse(input);
@@ -54,11 +60,20 @@ let ListReportRunSchedulesTool = class ListReportRunSchedulesTool {
54
60
  this.parseInput(input);
55
61
  const ctx = { projectId: context.projectId, userId: context.userId, roles: context.roles };
56
62
  const items = await this.facade.listReportRunSchedules(ctx);
63
+ const publicOrigin = this.publicOriginService.getPublicOrigin();
64
+ const schedulesUrl = (0, mcp_public_url_util_1.joinPublicOrigin)(publicOrigin, (0, data_mart_ui_path_1.buildReportSchedulesUiPath)(context.projectId));
57
65
  const structuredContent = {
58
66
  schedules: items.map(item => ({
59
67
  trigger_id: item.triggerId,
60
- report: item.report,
61
- data_mart: item.dataMart,
68
+ report: {
69
+ ...item.report,
70
+ url: (0, mcp_public_url_util_1.joinPublicOrigin)(publicOrigin, (0, data_mart_ui_path_1.buildReportsUiPath)(context.projectId, item.dataMart.id)),
71
+ },
72
+ data_mart: {
73
+ ...item.dataMart,
74
+ url: (0, mcp_public_url_util_1.joinPublicOrigin)(publicOrigin, (0, data_mart_ui_path_1.buildDataMartUiPath)(context.projectId, item.dataMart.id)),
75
+ },
76
+ schedules_url: schedulesUrl,
62
77
  cron_expression: item.cronExpression,
63
78
  time_zone: item.timeZone,
64
79
  is_active: item.isActive,
@@ -75,6 +90,6 @@ exports.ListReportRunSchedulesTool = ListReportRunSchedulesTool;
75
90
  exports.ListReportRunSchedulesTool = ListReportRunSchedulesTool = __decorate([
76
91
  (0, common_1.Injectable)(),
77
92
  __param(0, (0, common_1.Inject)(mcp_scheduled_triggers_facade_1.MCP_SCHEDULED_TRIGGERS_FACADE)),
78
- __metadata("design:paramtypes", [Object])
93
+ __metadata("design:paramtypes", [Object, public_origin_service_1.PublicOriginService])
79
94
  ], ListReportRunSchedulesTool);
80
95
  //# sourceMappingURL=list-report-run-schedules.tool.js.map
@@ -0,0 +1,7 @@
1
+ import type { McpProjectContextFacade } from '../../../idp/facades/mcp-project-context.facade';
2
+ import type { McpAuthContext } from '../auth/mcp-auth-context';
3
+ export interface McpProjectSummary {
4
+ id: string;
5
+ title: string;
6
+ }
7
+ export declare function tryGetMcpProjectSummary(projectContext: McpProjectContextFacade, context: McpAuthContext): Promise<McpProjectSummary | undefined>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryGetMcpProjectSummary = tryGetMcpProjectSummary;
4
+ async function tryGetMcpProjectSummary(projectContext, context) {
5
+ try {
6
+ const result = await projectContext.getProjectContext({
7
+ projectId: context.projectId,
8
+ userId: context.userId,
9
+ roles: context.roles,
10
+ });
11
+ return { id: result.project.id, title: result.project.title };
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ //# sourceMappingURL=mcp-project-summary.util.js.map
@@ -2,14 +2,16 @@ import { z } from 'zod';
2
2
  import type { McpScope } from '@owox/idp-protocol';
3
3
  import { type McpDataMartsFacade } from '../../../data-marts/facades/mcp-data-marts.facade';
4
4
  import { ClsContextService } from '../../../common/logger/cls-context.service';
5
+ import { PublicOriginService } from '../../../common/config/public-origin.service';
5
6
  import type { McpAuthContext } from '../auth/mcp-auth-context';
6
7
  import type { McpToolDefinition, McpToolResult } from './mcp-tool.definition';
7
8
  import { type QueryDataMartInput } from './query-data-mart.input';
8
9
  export declare class QueryDataMartTool implements McpToolDefinition<QueryDataMartInput> {
9
10
  private readonly dataMarts;
10
11
  private readonly cls;
12
+ private readonly publicOriginService;
11
13
  readonly name = "query_data_mart";
12
- readonly description = "Query an OWOX data mart and return its data rows in a compact, header-once table, plus server-side totals computed over all matching rows (ignoring the row limit). Each call costs credits.\n\nCall get_data_mart_details_by_id first to get the data mart's exact field names and joinable/blended fields, then copy field names verbatim into fields \u2014 unless you already have that schema in context. Field names must be exact; never guess or invent them.\n\nWhen building the query:\n- Request only the fields relevant to the user's question \u2014 never request all fields.\n- Use limit to control how many rows come back (1\u20131000, default 20). There is no offset/pagination: the tool returns a bounded subset.\n- aggregations: SUM, COUNT, COUNT_DISTINCT, AVG, MIN, MAX, and percentiles P25/P50/P75/P95 \u2014 but each data mart's output controls decide which functions a given field allows, so some may be rejected (pick another, or ask an admin to enable it). Group-by is implied by the non-aggregated fields you select.\n- date_buckets: bucket a date/timestamp field by DAY/WEEK/MONTH/QUARTER/YEAR (e.g. \"revenue by month\").\n- sort: order the result rows by { field, direction } with direction \"asc\" or \"desc\"; rules apply in order (the first is the primary key). Each sorted field must also be listed in fields.\n- fields must list every column the query uses, INCLUDING any field named in aggregations, date_buckets, or sort \u2014 a field you aggregate, bucket, or sort but omit from fields is rejected. Example \u2014 \"revenue by month\": fields [\"ts\", \"revenue\"], aggregations [{field: \"revenue\", function: \"SUM\"}], date_buckets [{field: \"ts\", unit: \"MONTH\"}]. (Filters are the exception: a filter may reference a field that is not in fields.)\n\nChoosing between slices and filters (both are row-level predicates applied to raw values BEFORE any aggregation \u2014 neither can threshold an aggregated total; there is no HAVING):\n- slices (pre-join): narrow a JOINED data mart before it is blended in \u2014 criteria on a joined data mart's own fields only. Slices do NOT apply to the main data mart. More efficient \u2014 they reduce the joined volume before the join. A slice runs on the field's ORIGINAL value, so when get_data_mart_details lists a \"sliceType\" for the field, use operators valid for that pre-join type (not the field's blended-result \"type\").\n- filters (post-join): row-level criteria on the blended result \u2014 use for anything on the MAIN data mart's fields or on a joined field. A filter on a field you also aggregate restricts which raw rows feed the aggregate (e.g. filter revenue > 0 \u2192 SUM over positive rows), NOT the group total.\n- Rule: pre-narrowing a joined data mart's rows \u2192 slices; any other raw-row criterion \u2192 filters.\n- Filtering by an aggregated total (e.g. \"groups whose SUM(revenue) > 100\") is NOT supported \u2014 return all groups with their totals and let the caller compare.\n- Example \u2014 \"orders in the last 3 months\" where orders is a joined data mart: the date range narrows the joined orders before the join \u2192 slices.\n\nUsing the results:\n- Use metrics and totals from the response directly \u2014 never recompute a value already present (totals are computed server-side over all matching rows, so they stay correct even when rows are truncated).\n- The totals block is separate from the rows.\n- When you use aggregations, the rows include an extra \"Row Count\" column \u2014 the number of underlying rows in each group. It is grouping metadata, not one of your requested fields; ignore it unless the user asked how many rows a group contains.\n\nIf truncated is true, not all matching rows were returned: narrow the query (fewer fields, tighter slices/filters) or raise limit (up to 1000).";
14
+ readonly description = "Query an OWOX data mart and return its data rows in a compact, header-once table, plus server-side totals computed over all matching rows (ignoring the row limit). Each call costs credits.\n\nCall get_data_mart_details_by_id first to get the data mart's exact field names and joinable/blended fields, then copy field names verbatim into fields \u2014 unless you already have that schema in context. Field names must be exact; never guess or invent them.\n\nWhen building the query:\n- Request only the fields relevant to the user's question \u2014 never request all fields.\n- Use limit to control how many rows come back (1\u20131000, default 20). There is no offset/pagination: the tool returns a bounded subset.\n- aggregations: SUM, COUNT, COUNT_DISTINCT, AVG, MIN, MAX, and percentiles P25/P50/P75/P95 \u2014 but each data mart's output controls decide which functions a given field allows, so some may be rejected (pick another, or ask an admin to enable it). Group-by is implied by the non-aggregated fields you select.\n- For \u201Chow many\u201D questions, use COUNT or COUNT_DISTINCT (when the business meaning is unique entities) instead of returning raw rows and counting them yourself. Keep only dimensions the user asked to break the count by.\n- date_buckets: bucket a date/timestamp field by DAY/WEEK/MONTH/QUARTER/YEAR (e.g. \"revenue by month\").\n- sort: order the result rows by { field, direction } with direction \"asc\" or \"desc\"; rules apply in order (the first is the primary key). Each sorted field must also be listed in fields.\n- fields must list every column the query uses, INCLUDING any field named in aggregations, date_buckets, or sort \u2014 a field you aggregate, bucket, or sort but omit from fields is rejected. Example \u2014 \"revenue by month\": fields [\"ts\", \"revenue\"], aggregations [{field: \"revenue\", function: \"SUM\"}], date_buckets [{field: \"ts\", unit: \"MONTH\"}]. (Filters are the exception: a filter may reference a field that is not in fields.)\n\nChoosing between slices and filters (both are row-level predicates applied to raw values BEFORE any aggregation \u2014 neither can threshold an aggregated total; there is no HAVING):\n- slices (pre-join): narrow a JOINED data mart before it is blended in \u2014 criteria on a joined data mart's own fields only. Slices do NOT apply to the main data mart. More efficient \u2014 they reduce the joined volume before the join. A slice runs on the field's ORIGINAL value, so when get_data_mart_details lists a \"sliceType\" for the field, use operators valid for that pre-join type (not the field's blended-result \"type\").\n- filters (post-join): row-level criteria on the blended result \u2014 use for anything on the MAIN data mart's fields or on a joined field. A filter on a field you also aggregate restricts which raw rows feed the aggregate (e.g. filter revenue > 0 \u2192 SUM over positive rows), NOT the group total.\n- Rule: pre-narrowing a joined data mart's rows \u2192 slices; any other raw-row criterion \u2192 filters.\n- Filtering by an aggregated total (e.g. \"groups whose SUM(revenue) > 100\") is NOT supported \u2014 return all groups with their totals and let the caller compare.\n- Example \u2014 \"orders in the last 3 months\" where orders is a joined data mart: the date range narrows the joined orders before the join \u2192 slices.\n\nUsing the results:\n- Use metrics and totals from the response directly \u2014 never recompute a value already present (totals are computed server-side over all matching rows, so they stay correct even when rows are truncated).\n- The totals block is separate from the rows.\n- Totals keys are technical output column names. Match a totals key to column_metadata[].name, not to a business-friendly columns label.\n- When you use aggregations, the rows include an extra \"Row Count\" column \u2014 the number of underlying rows in each group. It is grouping metadata, not one of your requested fields; ignore it unless the user asked how many rows a group contains.\n\nIf truncated is true, not all matching rows were returned: narrow the query (fewer fields, tighter slices/filters) or raise limit (up to 1000). Always use the source metadata in the response: name the Data Mart the answer came from, distinguish numbers calculated by OWOX from arithmetic you perform yourself, and clearly warn the user when rows were truncated.";
13
15
  readonly zodSchema: {
14
16
  data_mart_id: z.ZodString;
15
17
  fields: z.ZodArray<z.ZodString, "many">;
@@ -76,10 +78,71 @@ export declare class QueryDataMartTool implements McpToolDefinition<QueryDataMar
76
78
  };
77
79
  readonly outputSchema: {
78
80
  columns: z.ZodArray<z.ZodString, "many">;
81
+ column_metadata: z.ZodArray<z.ZodObject<{
82
+ name: z.ZodString;
83
+ display_name: z.ZodString;
84
+ description: z.ZodOptional<z.ZodString>;
85
+ type: z.ZodOptional<z.ZodString>;
86
+ }, "strip", z.ZodTypeAny, {
87
+ name: string;
88
+ display_name: string;
89
+ type?: string | undefined;
90
+ description?: string | undefined;
91
+ }, {
92
+ name: string;
93
+ display_name: string;
94
+ type?: string | undefined;
95
+ description?: string | undefined;
96
+ }>, "many">;
79
97
  rows: z.ZodString;
80
98
  returned_rows: z.ZodNumber;
81
99
  truncated: z.ZodBoolean;
100
+ truncation: z.ZodOptional<z.ZodObject<{
101
+ reasons: z.ZodArray<z.ZodEnum<["row_limit", "payload_byte_cap"]>, "many">;
102
+ }, "strip", z.ZodTypeAny, {
103
+ reasons: ("row_limit" | "payload_byte_cap")[];
104
+ }, {
105
+ reasons: ("row_limit" | "payload_byte_cap")[];
106
+ }>>;
82
107
  totals: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
108
+ source: z.ZodObject<{
109
+ data_mart: z.ZodObject<{
110
+ id: z.ZodString;
111
+ title: z.ZodString;
112
+ url: z.ZodString;
113
+ }, "strip", z.ZodTypeAny, {
114
+ id: string;
115
+ title: string;
116
+ url: string;
117
+ }, {
118
+ id: string;
119
+ title: string;
120
+ url: string;
121
+ }>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ data_mart: {
124
+ id: string;
125
+ title: string;
126
+ url: string;
127
+ };
128
+ }, {
129
+ data_mart: {
130
+ id: string;
131
+ title: string;
132
+ url: string;
133
+ };
134
+ }>;
135
+ calculation_origin: z.ZodObject<{
136
+ rows: z.ZodLiteral<"taken_from_owox">;
137
+ totals: z.ZodEnum<["calculated_by_owox", "not_available"]>;
138
+ }, "strip", z.ZodTypeAny, {
139
+ totals: "calculated_by_owox" | "not_available";
140
+ rows: "taken_from_owox";
141
+ }, {
142
+ totals: "calculated_by_owox" | "not_available";
143
+ rows: "taken_from_owox";
144
+ }>;
145
+ _instruction: z.ZodString;
83
146
  };
84
147
  readonly annotations: {
85
148
  title: string;
@@ -89,7 +152,7 @@ export declare class QueryDataMartTool implements McpToolDefinition<QueryDataMar
89
152
  openWorldHint: boolean;
90
153
  };
91
154
  readonly requiredScopes: McpScope[];
92
- constructor(dataMarts: McpDataMartsFacade, cls: ClsContextService);
155
+ constructor(dataMarts: McpDataMartsFacade, cls: ClsContextService, publicOriginService: PublicOriginService);
93
156
  private parseInput;
94
157
  handler(input: QueryDataMartInput, context: McpAuthContext, signal?: AbortSignal): Promise<McpToolResult>;
95
158
  private mapError;
@@ -20,14 +20,18 @@ const business_violation_exception_1 = require("../../../common/exceptions/busin
20
20
  const project_operation_blocked_exception_1 = require("../../../common/exceptions/project-operation-blocked.exception");
21
21
  const project_blocked_reason_enum_1 = require("../../../data-marts/enums/project-blocked-reason.enum");
22
22
  const cls_context_service_1 = require("../../../common/logger/cls-context.service");
23
+ const public_origin_service_1 = require("../../../common/config/public-origin.service");
23
24
  const mcp_tool_diagnostics_1 = require("../observability/mcp-tool-diagnostics");
24
25
  const query_data_mart_input_1 = require("./query-data-mart.input");
25
26
  const tabular_serializer_1 = require("./tabular-serializer");
26
27
  const output_controls_error_mapper_1 = require("./output-controls-error.mapper");
27
28
  const mcp_error_mapper_1 = require("../mappers/mcp-error.mapper");
29
+ const data_mart_ui_path_1 = require("./data-mart-ui-path");
30
+ const mcp_public_url_util_1 = require("./mcp-public-url.util");
28
31
  let QueryDataMartTool = class QueryDataMartTool {
29
32
  dataMarts;
30
33
  cls;
34
+ publicOriginService;
31
35
  name = 'query_data_mart';
32
36
  description = `Query an OWOX data mart and return its data rows in a compact, header-once table, plus server-side totals computed over all matching rows (ignoring the row limit). Each call costs credits.
33
37
 
@@ -37,6 +41,7 @@ When building the query:
37
41
  - Request only the fields relevant to the user's question — never request all fields.
38
42
  - Use limit to control how many rows come back (1–1000, default 20). There is no offset/pagination: the tool returns a bounded subset.
39
43
  - aggregations: SUM, COUNT, COUNT_DISTINCT, AVG, MIN, MAX, and percentiles P25/P50/P75/P95 — but each data mart's output controls decide which functions a given field allows, so some may be rejected (pick another, or ask an admin to enable it). Group-by is implied by the non-aggregated fields you select.
44
+ - For “how many” questions, use COUNT or COUNT_DISTINCT (when the business meaning is unique entities) instead of returning raw rows and counting them yourself. Keep only dimensions the user asked to break the count by.
40
45
  - date_buckets: bucket a date/timestamp field by DAY/WEEK/MONTH/QUARTER/YEAR (e.g. "revenue by month").
41
46
  - sort: order the result rows by { field, direction } with direction "asc" or "desc"; rules apply in order (the first is the primary key). Each sorted field must also be listed in fields.
42
47
  - fields must list every column the query uses, INCLUDING any field named in aggregations, date_buckets, or sort — a field you aggregate, bucket, or sort but omit from fields is rejected. Example — "revenue by month": fields ["ts", "revenue"], aggregations [{field: "revenue", function: "SUM"}], date_buckets [{field: "ts", unit: "MONTH"}]. (Filters are the exception: a filter may reference a field that is not in fields.)
@@ -51,16 +56,43 @@ Choosing between slices and filters (both are row-level predicates applied to ra
51
56
  Using the results:
52
57
  - Use metrics and totals from the response directly — never recompute a value already present (totals are computed server-side over all matching rows, so they stay correct even when rows are truncated).
53
58
  - The totals block is separate from the rows.
59
+ - Totals keys are technical output column names. Match a totals key to column_metadata[].name, not to a business-friendly columns label.
54
60
  - When you use aggregations, the rows include an extra "Row Count" column — the number of underlying rows in each group. It is grouping metadata, not one of your requested fields; ignore it unless the user asked how many rows a group contains.
55
61
 
56
- If truncated is true, not all matching rows were returned: narrow the query (fewer fields, tighter slices/filters) or raise limit (up to 1000).`;
62
+ If truncated is true, not all matching rows were returned: narrow the query (fewer fields, tighter slices/filters) or raise limit (up to 1000). Always use the source metadata in the response: name the Data Mart the answer came from, distinguish numbers calculated by OWOX from arithmetic you perform yourself, and clearly warn the user when rows were truncated.`;
57
63
  zodSchema = query_data_mart_input_1.queryDataMartInputSchema.shape;
58
64
  outputSchema = {
59
- columns: zod_1.z.array(zod_1.z.string()),
65
+ columns: zod_1.z.array(zod_1.z.string()).describe('Business-friendly result headers, in rows order.'),
66
+ column_metadata: zod_1.z.array(zod_1.z.object({
67
+ name: zod_1.z.string().describe('Exact output column name.'),
68
+ display_name: zod_1.z.string().describe('Business-friendly label for presentation.'),
69
+ description: zod_1.z.string().optional(),
70
+ type: zod_1.z.string().optional(),
71
+ })),
60
72
  rows: zod_1.z.string(),
61
73
  returned_rows: zod_1.z.number(),
62
74
  truncated: zod_1.z.boolean(),
63
- totals: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullable(),
75
+ truncation: zod_1.z
76
+ .object({
77
+ reasons: zod_1.z.array(zod_1.z.enum(['row_limit', 'payload_byte_cap'])).min(1),
78
+ })
79
+ .optional(),
80
+ totals: zod_1.z
81
+ .record(zod_1.z.string(), zod_1.z.unknown())
82
+ .nullable()
83
+ .describe('Server-side totals keyed by technical output column name. Match each key to column_metadata[].name.'),
84
+ source: zod_1.z.object({
85
+ data_mart: zod_1.z.object({
86
+ id: zod_1.z.string(),
87
+ title: zod_1.z.string(),
88
+ url: zod_1.z.string(),
89
+ }),
90
+ }),
91
+ calculation_origin: zod_1.z.object({
92
+ rows: zod_1.z.literal('taken_from_owox'),
93
+ totals: zod_1.z.enum(['calculated_by_owox', 'not_available']),
94
+ }),
95
+ _instruction: zod_1.z.string(),
64
96
  };
65
97
  annotations = {
66
98
  title: 'Query Data Mart',
@@ -70,9 +102,10 @@ If truncated is true, not all matching rows were returned: narrow the query (few
70
102
  openWorldHint: false,
71
103
  };
72
104
  requiredScopes = ['mcp:read', 'mcp:write'];
73
- constructor(dataMarts, cls) {
105
+ constructor(dataMarts, cls, publicOriginService) {
74
106
  this.dataMarts = dataMarts;
75
107
  this.cls = cls;
108
+ this.publicOriginService = publicOriginService;
76
109
  }
77
110
  parseInput(input) {
78
111
  return query_data_mart_input_1.queryDataMartInputSchema.parse(input);
@@ -103,13 +136,43 @@ If truncated is true, not all matching rows were returned: narrow the query (few
103
136
  catch {
104
137
  }
105
138
  }
106
- const { tsv, rowCount, capped } = (0, tabular_serializer_1.serializeTsvWithByteCap)(res.columns, res.rows, tabular_serializer_1.ROWS_PAYLOAD_BYTE_CAP);
139
+ const displayColumns = (0, tabular_serializer_1.formatTsvColumnLabels)(res.columnMetadata);
140
+ const { tsv, headerColumns, rowCount, capped } = (0, tabular_serializer_1.serializeTsvWithByteCap)(displayColumns, res.rows, tabular_serializer_1.ROWS_PAYLOAD_BYTE_CAP);
141
+ const truncationReasons = [
142
+ ...(res.truncated ? ['row_limit'] : []),
143
+ ...(capped ? ['payload_byte_cap'] : []),
144
+ ];
145
+ const isTruncated = truncationReasons.length > 0;
146
+ const totalsKeyInstruction = res.totals
147
+ ? ' Totals keys are technical output names; match them to column_metadata[].name, not to display labels.'
148
+ : '';
107
149
  const structuredContent = {
108
- columns: res.columns,
150
+ columns: headerColumns,
151
+ column_metadata: res.columnMetadata.map((column, index) => ({
152
+ name: column.name,
153
+ display_name: headerColumns[index],
154
+ ...(column.description ? { description: column.description } : {}),
155
+ ...(column.type ? { type: column.type } : {}),
156
+ })),
109
157
  rows: tsv,
110
158
  returned_rows: rowCount,
111
- truncated: res.truncated || capped,
159
+ truncated: isTruncated,
160
+ ...(isTruncated ? { truncation: { reasons: truncationReasons } } : {}),
112
161
  totals: res.totals,
162
+ source: {
163
+ data_mart: {
164
+ id: res.dataMart.id,
165
+ title: res.dataMart.title,
166
+ url: (0, mcp_public_url_util_1.joinPublicOrigin)(this.publicOriginService.getPublicOrigin(), (0, data_mart_ui_path_1.buildDataMartUiPath)(context.projectId, res.dataMart.id)),
167
+ },
168
+ },
169
+ calculation_origin: {
170
+ rows: 'taken_from_owox',
171
+ totals: res.totals ? 'calculated_by_owox' : 'not_available',
172
+ },
173
+ _instruction: isTruncated
174
+ ? `IMPORTANT: Rows are incomplete. Tell the user explicitly that the result was truncated and that any conclusion based on rows may be incomplete. State the Data Mart source. Server-provided totals still cover all matching rows; do not describe a value you calculate from returned rows as an OWOX-calculated total.${totalsKeyInstruction}`
175
+ : `State which Data Mart supplied the data. Identify server-provided rows and totals as taken from or calculated by OWOX. If you perform arithmetic from those values yourself, label it as an AI-side calculation.${totalsKeyInstruction}`,
113
176
  };
114
177
  return {
115
178
  structuredContent,
@@ -179,6 +242,7 @@ exports.QueryDataMartTool = QueryDataMartTool;
179
242
  exports.QueryDataMartTool = QueryDataMartTool = __decorate([
180
243
  (0, common_1.Injectable)(),
181
244
  __param(0, (0, common_1.Inject)(mcp_data_marts_facade_1.MCP_DATA_MARTS_FACADE)),
182
- __metadata("design:paramtypes", [Object, cls_context_service_1.ClsContextService])
245
+ __metadata("design:paramtypes", [Object, cls_context_service_1.ClsContextService,
246
+ public_origin_service_1.PublicOriginService])
183
247
  ], QueryDataMartTool);
184
248
  //# sourceMappingURL=query-data-mart.tool.js.map
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import type { McpScope } from '@owox/idp-protocol';
3
3
  import { type SearchFacade } from '../../../common/search/search.facade';
4
4
  import { PublicOriginService } from '../../../common/config/public-origin.service';
5
+ import { type McpProjectContextFacade } from '../../../idp/facades/mcp-project-context.facade';
5
6
  import type { McpAuthContext } from '../auth/mcp-auth-context';
6
7
  import { type McpToolDefinition, type McpToolResult } from './mcp-tool.definition';
7
8
  declare const inputSchema: z.ZodObject<{
@@ -18,6 +19,7 @@ type SearchDataMartsInput = z.infer<typeof inputSchema>;
18
19
  export declare class SearchDataMartsTool implements McpToolDefinition<SearchDataMartsInput> {
19
20
  private readonly searchFacade;
20
21
  private readonly publicOriginService;
22
+ private readonly projectContext;
21
23
  readonly name = "get_relevant_data_marts_by_prompt";
22
24
  readonly description = "Find relevant non-draft data marts in the current OWOX project from a natural-language prompt, limited to data marts visible to the current MCP user. This is the default discovery step for a concrete analytical question when the data mart has not already been confirmed. Use it when the user asks to find, discover, or search published data marts by title, description, business meaning, schema fields, or metrics. This tool returns only data marts, not data storages or destinations, and it intentionally excludes draft data marts.";
23
25
  readonly zodSchema: {
@@ -25,6 +27,16 @@ export declare class SearchDataMartsTool implements McpToolDefinition<SearchData
25
27
  limit: z.ZodOptional<z.ZodNumber>;
26
28
  };
27
29
  readonly outputSchema: {
30
+ project: z.ZodOptional<z.ZodObject<{
31
+ id: z.ZodString;
32
+ title: z.ZodString;
33
+ }, "strip", z.ZodTypeAny, {
34
+ id: string;
35
+ title: string;
36
+ }, {
37
+ id: string;
38
+ title: string;
39
+ }>>;
28
40
  data_marts: z.ZodArray<z.ZodObject<{
29
41
  id: z.ZodString;
30
42
  title: z.ZodString;
@@ -52,7 +64,7 @@ export declare class SearchDataMartsTool implements McpToolDefinition<SearchData
52
64
  openWorldHint: boolean;
53
65
  };
54
66
  readonly requiredScopes: McpScope[];
55
- constructor(searchFacade: SearchFacade, publicOriginService: PublicOriginService);
67
+ constructor(searchFacade: SearchFacade, publicOriginService: PublicOriginService, projectContext: McpProjectContextFacade);
56
68
  parseInput(input: unknown): SearchDataMartsInput;
57
69
  handler(input: SearchDataMartsInput, context: McpAuthContext): Promise<McpToolResult>;
58
70
  }
@@ -17,8 +17,10 @@ const common_1 = require("@nestjs/common");
17
17
  const zod_1 = require("zod");
18
18
  const search_facade_1 = require("../../../common/search/search.facade");
19
19
  const public_origin_service_1 = require("../../../common/config/public-origin.service");
20
+ const mcp_project_context_facade_1 = require("../../../idp/facades/mcp-project-context.facade");
20
21
  const mcp_tool_definition_1 = require("./mcp-tool.definition");
21
22
  const data_mart_ui_path_1 = require("./data-mart-ui-path");
23
+ const mcp_project_summary_util_1 = require("./mcp-project-summary.util");
22
24
  const mcp_public_url_util_1 = require("./mcp-public-url.util");
23
25
  const DEFAULT_LIMIT = 10;
24
26
  const MAX_LIMIT = 25;
@@ -31,10 +33,12 @@ const inputSchema = zod_1.z
31
33
  let SearchDataMartsTool = class SearchDataMartsTool {
32
34
  searchFacade;
33
35
  publicOriginService;
36
+ projectContext;
34
37
  name = 'get_relevant_data_marts_by_prompt';
35
38
  description = 'Find relevant non-draft data marts in the current OWOX project from a natural-language prompt, limited to data marts visible to the current MCP user. This is the default discovery step for a concrete analytical question when the data mart has not already been confirmed. Use it when the user asks to find, discover, or search published data marts by title, description, business meaning, schema fields, or metrics. This tool returns only data marts, not data storages or destinations, and it intentionally excludes draft data marts.';
36
39
  zodSchema = inputSchema.shape;
37
40
  outputSchema = {
41
+ project: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string() }).optional(),
38
42
  data_marts: zod_1.z.array(zod_1.z.object({
39
43
  id: zod_1.z.string(),
40
44
  title: zod_1.z.string(),
@@ -50,26 +54,31 @@ let SearchDataMartsTool = class SearchDataMartsTool {
50
54
  openWorldHint: false,
51
55
  };
52
56
  requiredScopes = ['mcp:read'];
53
- constructor(searchFacade, publicOriginService) {
57
+ constructor(searchFacade, publicOriginService, projectContext) {
54
58
  this.searchFacade = searchFacade;
55
59
  this.publicOriginService = publicOriginService;
60
+ this.projectContext = projectContext;
56
61
  }
57
62
  parseInput(input) {
58
63
  return inputSchema.parse(input);
59
64
  }
60
65
  async handler(input, context) {
61
66
  const parsed = this.parseInput(input);
62
- const results = await this.searchFacade.search(context.projectId, parsed.prompt, {
63
- topK: parsed.limit ?? DEFAULT_LIMIT,
64
- entityTypes: [search_facade_1.SearchableEntityType.DATA_MART],
65
- excludeDrafts: true,
66
- accessScope: {
67
- userId: context.userId,
68
- roles: context.roles,
69
- },
70
- });
67
+ const [results, projectContext] = await Promise.all([
68
+ this.searchFacade.search(context.projectId, parsed.prompt, {
69
+ topK: parsed.limit ?? DEFAULT_LIMIT,
70
+ entityTypes: [search_facade_1.SearchableEntityType.DATA_MART],
71
+ excludeDrafts: true,
72
+ accessScope: {
73
+ userId: context.userId,
74
+ roles: context.roles,
75
+ },
76
+ }),
77
+ (0, mcp_project_summary_util_1.tryGetMcpProjectSummary)(this.projectContext, context),
78
+ ]);
71
79
  const publicOrigin = this.publicOriginService.getPublicOrigin();
72
80
  const structuredContent = {
81
+ ...(projectContext ? { project: projectContext } : {}),
73
82
  data_marts: results
74
83
  .filter(result => result.entityType === search_facade_1.SearchableEntityType.DATA_MART)
75
84
  .map(result => ({
@@ -87,6 +96,7 @@ exports.SearchDataMartsTool = SearchDataMartsTool;
87
96
  exports.SearchDataMartsTool = SearchDataMartsTool = __decorate([
88
97
  (0, common_1.Injectable)(),
89
98
  __param(0, (0, common_1.Inject)(search_facade_1.SEARCH_FACADE)),
90
- __metadata("design:paramtypes", [Object, public_origin_service_1.PublicOriginService])
99
+ __param(2, (0, common_1.Inject)(mcp_project_context_facade_1.MCP_PROJECT_CONTEXT_FACADE)),
100
+ __metadata("design:paramtypes", [Object, public_origin_service_1.PublicOriginService, Object])
91
101
  ], SearchDataMartsTool);
92
102
  //# sourceMappingURL=search-data-marts.tool.js.map
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpScope } from '@owox/idp-protocol';
3
3
  import { PublicOriginService } from '../../../common/config/public-origin.service';
4
+ import { type McpProjectContextFacade } from '../../../idp/facades/mcp-project-context.facade';
4
5
  import { type McpDataMartsFacade } from '../../../data-marts/facades/mcp-data-marts.facade';
5
6
  import type { McpAuthContext } from '../auth/mcp-auth-context';
6
7
  import type { McpToolDefinition, McpToolResult } from './mcp-tool.definition';
@@ -9,10 +10,21 @@ type SummarizeDataCatalogInput = z.infer<typeof inputSchema>;
9
10
  export declare class SummarizeDataCatalogTool implements McpToolDefinition<SummarizeDataCatalogInput> {
10
11
  private readonly dataMarts;
11
12
  private readonly publicOriginService;
13
+ private readonly projectContext;
12
14
  readonly name = "summarize_data_catalog";
13
15
  readonly description = "Returns a high-level summary input for the current OWOX project published Data Mart catalog so the LLM can orient the user. Use when the user asks open-ended questions like \"what data is available here?\", \"what can I analyze?\", or \"where should I start?\". The tool returns counts and top published Data Marts ranked by configured relationship connectivity, with shortened descriptions and basic usage metadata. It does not query actual data rows, compute data freshness, or generate a natural-language summary.";
14
16
  readonly zodSchema: {};
15
17
  readonly outputSchema: {
18
+ project: z.ZodOptional<z.ZodObject<{
19
+ id: z.ZodString;
20
+ title: z.ZodString;
21
+ }, "strip", z.ZodTypeAny, {
22
+ id: string;
23
+ title: string;
24
+ }, {
25
+ id: string;
26
+ title: string;
27
+ }>>;
16
28
  project_id: z.ZodString;
17
29
  data_mart_count: z.ZodNumber;
18
30
  top_data_marts_by_connectivity: z.ZodArray<z.ZodObject<{
@@ -52,7 +64,7 @@ export declare class SummarizeDataCatalogTool implements McpToolDefinition<Summa
52
64
  openWorldHint: boolean;
53
65
  };
54
66
  readonly requiredScopes: McpScope[];
55
- constructor(dataMarts: McpDataMartsFacade, publicOriginService: PublicOriginService);
67
+ constructor(dataMarts: McpDataMartsFacade, publicOriginService: PublicOriginService, projectContext: McpProjectContextFacade);
56
68
  parseInput(input: unknown): SummarizeDataCatalogInput;
57
69
  handler(input: SummarizeDataCatalogInput, context: McpAuthContext): Promise<McpToolResult>;
58
70
  }
@@ -16,18 +16,22 @@ exports.SummarizeDataCatalogTool = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const zod_1 = require("zod");
18
18
  const public_origin_service_1 = require("../../../common/config/public-origin.service");
19
+ const mcp_project_context_facade_1 = require("../../../idp/facades/mcp-project-context.facade");
19
20
  const mcp_data_marts_facade_1 = require("../../../data-marts/facades/mcp-data-marts.facade");
20
21
  const data_mart_ui_path_1 = require("./data-mart-ui-path");
22
+ const mcp_project_summary_util_1 = require("./mcp-project-summary.util");
21
23
  const mcp_public_url_util_1 = require("./mcp-public-url.util");
22
24
  const inputSchema = zod_1.z.object({}).strict();
23
25
  const INSTRUCTION = 'You have received a high-level summary of the published Data Mart catalog available to this MCP connection. Summarize the business areas covered by the listed Data Marts and suggest 4-6 concrete example prompts the user could ask. Do not claim access to data rows, sample values, row counts, or freshness details.';
24
26
  let SummarizeDataCatalogTool = class SummarizeDataCatalogTool {
25
27
  dataMarts;
26
28
  publicOriginService;
29
+ projectContext;
27
30
  name = 'summarize_data_catalog';
28
31
  description = 'Returns a high-level summary input for the current OWOX project published Data Mart catalog so the LLM can orient the user. Use when the user asks open-ended questions like "what data is available here?", "what can I analyze?", or "where should I start?". The tool returns counts and top published Data Marts ranked by configured relationship connectivity, with shortened descriptions and basic usage metadata. It does not query actual data rows, compute data freshness, or generate a natural-language summary.';
29
32
  zodSchema = inputSchema.shape;
30
33
  outputSchema = {
34
+ project: zod_1.z.object({ id: zod_1.z.string(), title: zod_1.z.string() }).optional(),
31
35
  project_id: zod_1.z.string(),
32
36
  data_mart_count: zod_1.z.number(),
33
37
  top_data_marts_by_connectivity: zod_1.z.array(zod_1.z.object({
@@ -49,22 +53,27 @@ let SummarizeDataCatalogTool = class SummarizeDataCatalogTool {
49
53
  openWorldHint: false,
50
54
  };
51
55
  requiredScopes = ['mcp:read'];
52
- constructor(dataMarts, publicOriginService) {
56
+ constructor(dataMarts, publicOriginService, projectContext) {
53
57
  this.dataMarts = dataMarts;
54
58
  this.publicOriginService = publicOriginService;
59
+ this.projectContext = projectContext;
55
60
  }
56
61
  parseInput(input) {
57
62
  return inputSchema.parse(input);
58
63
  }
59
64
  async handler(input, context) {
60
65
  this.parseInput(input);
61
- const result = await this.dataMarts.summarizeDataCatalog({
62
- projectId: context.projectId,
63
- userId: context.userId,
64
- roles: context.roles,
65
- });
66
+ const [result, projectContext] = await Promise.all([
67
+ this.dataMarts.summarizeDataCatalog({
68
+ projectId: context.projectId,
69
+ userId: context.userId,
70
+ roles: context.roles,
71
+ }),
72
+ (0, mcp_project_summary_util_1.tryGetMcpProjectSummary)(this.projectContext, context),
73
+ ]);
66
74
  const publicOrigin = this.publicOriginService.getPublicOrigin();
67
75
  const structuredContent = {
76
+ ...(projectContext ? { project: projectContext } : {}),
68
77
  project_id: result.projectId,
69
78
  data_mart_count: result.dataMartCount,
70
79
  top_data_marts_by_connectivity: result.topDataMartsByConnectivity.map(dataMart => ({
@@ -94,6 +103,7 @@ exports.SummarizeDataCatalogTool = SummarizeDataCatalogTool;
94
103
  exports.SummarizeDataCatalogTool = SummarizeDataCatalogTool = __decorate([
95
104
  (0, common_1.Injectable)(),
96
105
  __param(0, (0, common_1.Inject)(mcp_data_marts_facade_1.MCP_DATA_MARTS_FACADE)),
97
- __metadata("design:paramtypes", [Object, public_origin_service_1.PublicOriginService])
106
+ __param(2, (0, common_1.Inject)(mcp_project_context_facade_1.MCP_PROJECT_CONTEXT_FACADE)),
107
+ __metadata("design:paramtypes", [Object, public_origin_service_1.PublicOriginService, Object])
98
108
  ], SummarizeDataCatalogTool);
99
109
  //# sourceMappingURL=summarize-data-catalog.tool.js.map
@@ -1,6 +1,12 @@
1
1
  export declare const ROWS_PAYLOAD_BYTE_CAP = 131072;
2
+ export interface TsvColumnLabel {
3
+ name: string;
4
+ displayName: string;
5
+ }
6
+ export declare function formatTsvColumnLabels(columns: readonly TsvColumnLabel[]): string[];
2
7
  export declare function serializeTsvWithByteCap(columns: string[], rows: unknown[][], maxBytes: number): {
3
8
  tsv: string;
9
+ headerColumns: string[];
4
10
  rowCount: number;
5
11
  capped: boolean;
6
12
  };