From 0a4196dbc1dfe99c286693ee3a86a3c0d19e9d96 Mon Sep 17 00:00:00 2001 From: Madhav Bhagat Date: Wed, 6 May 2026 18:39:18 +0530 Subject: [PATCH 1/2] feat: add run_analytics_query tool and related documentation - Introduced `run_analytics_query` tool for executing SQL analytics against contract data. - Added detailed documentation for the tool in AGENTS.md and analytics_query_docs.ts. - Implemented error handling for redirect responses in SpotDraftClient. - Updated README.md and AGENTS.md to include the new analytics tool. --- README.md | 1 + src/spotdraft_client.ts | 20 +++++++ src/tools/AGENTS.md | 2 + src/tools/analytics/AGENTS.md | 14 +++++ src/tools/analytics/analytics_query_docs.ts | 65 +++++++++++++++++++++ src/tools/analytics/run_analytics_query.ts | 57 ++++++++++++++++++ src/tools/index.ts | 8 +++ 7 files changed, 167 insertions(+) create mode 100644 src/tools/analytics/AGENTS.md create mode 100644 src/tools/analytics/analytics_query_docs.ts create mode 100644 src/tools/analytics/run_analytics_query.ts diff --git a/README.md b/README.md index bbd65fb..08fdf02 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ These tools are only listed and callable when write mode is enabled (see [Write - `get_key_pointers` — Retrieves key pointers - `get_contract_types` — Retrieves available contract types +- `run_analytics_query` — Run SQL analytics against contract data ## Setup diff --git a/src/spotdraft_client.ts b/src/spotdraft_client.ts index ff2e157..67e4078 100644 --- a/src/spotdraft_client.ts +++ b/src/spotdraft_client.ts @@ -120,6 +120,20 @@ export class SpotDraftClient { return `SpotDraft API Error: ${statusText}`; } + /** Ensures we never silently follow redirects (POST would otherwise become GET). Caller must await before reading the body. */ + private async rejectIfRedirect(response: Response, method: string, url: string, endpoint: string): Promise { + if (response.status < 300 || response.status >= 400) { + return; + } + await response.arrayBuffer().catch(() => undefined); + const location = response.headers.get('location'); + throw new APIError( + `SpotDraft API returned HTTP ${response.status} redirect (${location ?? 'missing Location header'}). Set baseUrl to the canonical API URL so requests do not redirect (scheme, host, path, trailing slashes).`, + 502, + { endpoint, method, redirectStatus: response.status, location, requestUrl: url }, + ); + } + async get(endpoint: string, queryParams?: URLSearchParams) { const url = queryParams ? `${this.baseUrl}${endpoint}?${queryParams.toString()}` : `${this.baseUrl}${endpoint}`; @@ -129,8 +143,11 @@ export class SpotDraftClient { const response = await fetch(url, { method: 'GET', headers: this.headers, + redirect: 'manual', }); + await this.rejectIfRedirect(response, 'GET', url, endpoint); + if (!response.ok) { const errorBody = await response.text(); this.logger.error('SpotDraftClient', `API request failed`, undefined, { @@ -179,8 +196,11 @@ export class SpotDraftClient { method: 'POST', headers: this.headers, body: body ? JSON.stringify(body) : undefined, + redirect: 'manual', }); + await this.rejectIfRedirect(response, 'POST', url, endpoint); + if (!response.ok) { const errorBody = await response.text(); this.logger.error('SpotDraftClient', `API request failed`, undefined, { diff --git a/src/tools/AGENTS.md b/src/tools/AGENTS.md index ee2ac1d..2ea37de 100644 --- a/src/tools/AGENTS.md +++ b/src/tools/AGENTS.md @@ -51,6 +51,8 @@ - Legal Intake: - `get_legal_intakes` - `get_legal_intake_by_id` +- Analytics: + - `run_analytics_query` ## Registry Invariants diff --git a/src/tools/analytics/AGENTS.md b/src/tools/analytics/AGENTS.md new file mode 100644 index 0000000..c211e80 --- /dev/null +++ b/src/tools/analytics/AGENTS.md @@ -0,0 +1,14 @@ +# AGENTS.md + +## Read This When + +- You are editing the analytics query tool, embedded catalog summary, or changing the upstream `POST /v2.1/public/analytics/query/` contract. + +## Public Surface + +- `run_analytics_query.ts` — MCP tool and handler; forwards to SpotDraft with optional `user_scoped`. +- `analytics_query_docs.ts` — `buildRunAnalyticsQueryDescription()`: embedded OpenAPI-aligned schema summary (allowlisted tables, limits, errors). **Keep in sync** with `paths['/api/v2.1/public/analytics/query/'].post.description` in the regional `https:///api/docs/schema/` when the catalog copy changes. + +## Upstream + +- `POST` `/v2.1/public/analytics/query/` — read-only GoogleSQL; request body `query` (required), `user_scoped` (optional). diff --git a/src/tools/analytics/analytics_query_docs.ts b/src/tools/analytics/analytics_query_docs.ts new file mode 100644 index 0000000..f70fc40 --- /dev/null +++ b/src/tools/analytics/analytics_query_docs.ts @@ -0,0 +1,65 @@ +/** + * Embedded summary for `run_analytics_query` tool descriptions. + * Keep in sync with `paths['/api/v2.1/public/analytics/query/'].post.description` + * in https://api.in.spotdraft.com/api/docs/schema/ when the deployment catalog changes. + */ + +const ANALYTICS_TABLE_CATALOG_URL = 'https://api.in.spotdraft.com/api/docs#description/analytics-query-table-catalog'; +const ANALYTICS_TAG_URL = 'https://api.in.spotdraft.com/api/docs#tag/v21-analytics-query'; + +/** Upstream OpenAPI operation description (Scalar-specific anchor link replaced with a public URL). */ +const ANALYTICS_QUERY_OPERATION_SUMMARY = `Runs **read-only** analytics SQL in **GoogleSQL** (Google Standard SQL), including for timestamps, dates, and functions. Submit exactly one **\`SELECT\`** statement (optional **\`WITH\`** / **\`UNION\`**). Inserts, updates, deletes, DDL, and multi-statement batches are rejected. + +**Enablement** +This capability must be enabled for your workspace. If it is not, the API returns **501** with code \`SD_PUBLIC_00045\`. Contact SpotDraft support to request access. + +**Permission** +**Insights view** (\`INSIGHTS_VIEW\`). + +**Contracts scoping** +By default, the **\`Contracts\`** table is limited to rows the authenticated user may access. Optional body field **\`user_scoped\`** or header **\`X-Spotdraft-Analytics-User-Scoped\`** (\`true\` / \`false\`, or \`1\` / \`0\`, \`yes\` / \`no\`) overrides that. When both are omitted, the default is **\`true\`** in this environment. **\`user_scoped: false\`** (or the header equivalent) allows workspace-wide contract rows only for users with the workspace **Admin** role; others receive **403** with \`SD_PUBLIC_00044\`. + +**Tables and columns** +Use **bare catalog table names** (e.g. \`Contracts\`, \`Users\`) and **snake_case** public column names from the table catalog (e.g. \`execution_time\`, \`workflow_id\`). Only allowlisted tables and columns are accepted. + +**Allowlisted tables (quick reference)** + +| Table in SQL | Role | +|---|---| +| \`Contracts\` | Core contract fact table | +| \`Users\` | Users / org users | +| \`Workflows_setup\` | Workflow definitions (incl. frozen versions) | +| \`Contract_types_setup\` | Contract type lookup | +| \`Counterparties_setup\` | Counterparty directory | +| \`Approvals_Setup\` | Approval steps in workflows | +| \`Entities_setup\` | Legal entities | +| \`Signatures_setups\` | Signatory configuration per workflow | + +For full column lists, types, and join guidance, see the [SpotDraft public analytics table catalog](${ANALYTICS_TABLE_CATALOG_URL}). + +**Limits (this deployment)** +- At most **5,000** rows per response; \`truncated\` is **true** when more rows would have matched. +- Query job timeout: **60 seconds**. +- Maximum bytes billed per query job: **500 MB** (500,000,000 bytes). +- SQL text length limit: **10,000** characters per request. + +**Errors** + +| Code | HTTP | Meaning | +|---|---:|---| +| \`SD_PUBLIC_00040\` | 400 | Not an allowed read-only SELECT, or more than one statement | +| \`SD_PUBLIC_00041\` | 400 | References a table outside the allowlisted catalog | +| \`SD_PUBLIC_00042\` | 400 | Query could not be parsed or transpiled safely | +| \`SD_PUBLIC_00043\` | 502 | Query execution failed | +| \`SD_PUBLIC_00044\` | 403 | Workspace-wide contracts requested without workspace Admin role | +| \`SD_PUBLIC_00045\` | 501 | Analytics query API not enabled for this workspace | + +[V2.1 Analytics Query API reference](${ANALYTICS_TAG_URL})`; + +export function buildRunAnalyticsQueryDescription(): string { + return `Execute read-only SpotDraft analytics SQL (GoogleSQL). + +**When to use:** Custom reporting, metrics, or joins across allowlisted analytics tables when contracts workspace analytics is enabled for your org. + +${ANALYTICS_QUERY_OPERATION_SUMMARY}`; +} diff --git a/src/tools/analytics/run_analytics_query.ts b/src/tools/analytics/run_analytics_query.ts new file mode 100644 index 0000000..a01e47a --- /dev/null +++ b/src/tools/analytics/run_analytics_query.ts @@ -0,0 +1,57 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { ValidationError } from '../../errors/index.js'; +import { SpotDraftClient } from '../../spotdraft_client.js'; +import { buildRunAnalyticsQueryDescription } from './analytics_query_docs.js'; + +const MAX_QUERY_LENGTH = 10000; + +const runAnalyticsQueryTool: Tool = { + name: 'run_analytics_query', + description: buildRunAnalyticsQueryDescription(), + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: + 'A single GoogleSQL SELECT (optional WITH / UNION). Max 10,000 characters. Only allowlisted tables/columns; see tool description.', + }, + user_scoped: { + type: 'boolean', + description: + 'When true (default when omitted), restrict Contracts to rows the caller may access. When false, workspace-wide Contracts only for workspace Admin; others get 403.', + }, + }, + required: ['query'], + }, +}; + +export interface RunAnalyticsQueryRequest { + query: string; + user_scoped?: boolean; +} + +const runAnalyticsQuery = async ( + request: RunAnalyticsQueryRequest, + spotdraftClient: SpotDraftClient, +): Promise => { + const query = request.query?.trim() ?? ''; + if (query === '') { + throw new ValidationError('query is required and cannot be empty.', { field: 'query' }); + } + if (query.length > MAX_QUERY_LENGTH) { + throw new ValidationError(`query exceeds maximum length of ${MAX_QUERY_LENGTH} characters.`, { + field: 'query', + length: query.length, + }); + } + + const body: { query: string; user_scoped?: boolean } = { query }; + if (typeof request.user_scoped === 'boolean') { + body.user_scoped = request.user_scoped; + } + + return spotdraftClient.post('/v2.1/public/analytics/query', body); +}; + +export { runAnalyticsQuery, runAnalyticsQueryTool }; diff --git a/src/tools/index.ts b/src/tools/index.ts index 3139f09..8ea082b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -49,6 +49,9 @@ import { getTemplateDetails, getTemplateDetailsTool } from './templates/get_temp import { getTemplateMetadata, getTemplateMetadataTool } from './templates/get_template_metadata.js'; import { getTemplates, getTemplatesTool } from './templates/get_templates.js'; +// Analytics tools +import { runAnalyticsQuery, runAnalyticsQueryTool } from './analytics/run_analytics_query.js'; + /** * Global tool registry instance * All tools are registered here for centralized management @@ -159,6 +162,11 @@ toolRegistry.register({ handler: withSpotDraftClient(getLegalIntakeById), }); +toolRegistry.register({ + tool: runAnalyticsQueryTool, + handler: withSpotDraftClient(runAnalyticsQuery), +}); + // Write tools (only available when write mode is enabled) toolRegistry.register({ tool: uploadContractVersionTool, From 1e9c7aef22311d647f4b8140d4389bf213c1209a Mon Sep 17 00:00:00 2001 From: Madhav Bhagat Date: Wed, 6 May 2026 18:45:03 +0530 Subject: [PATCH 2/2] feat: introduce get_analytics_catalog tool and update documentation - Added `get_analytics_catalog` tool for retrieving allowlisted analytics tables and column schemas. - Updated README.md and AGENTS.md to include the new analytics tool. - Enhanced analytics_query_docs.ts with guidance on using `get_analytics_catalog` for accurate schema discovery. - Revised AGENTS.md in the analytics directory to reflect the new tool and its usage. --- README.md | 6 +++- src/tools/AGENTS.md | 1 + src/tools/analytics/AGENTS.md | 6 ++-- src/tools/analytics/analytics_query_docs.ts | 6 ++-- src/tools/analytics/get_analytics_catalog.ts | 33 ++++++++++++++++++++ src/tools/index.ts | 6 ++++ 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/tools/analytics/get_analytics_catalog.ts diff --git a/README.md b/README.md index 08fdf02..179bcec 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,15 @@ These tools are only listed and callable when write mode is enabled (see [Write - `get_legal_intakes` — List all legal intakes - `get_legal_intake_by_id` — Get specific legal intake by id +### Analytics + +- `get_analytics_catalog` — List allowlisted analytics tables and column schemas (GoogleSQL); optional `search` to narrow by table or column name/description +- `run_analytics_query` — Execute read-only GoogleSQL against analytics data; call `get_analytics_catalog` first when you need authoritative table/column names, types, or descriptions for this workspace + ### Other - `get_key_pointers` — Retrieves key pointers - `get_contract_types` — Retrieves available contract types -- `run_analytics_query` — Run SQL analytics against contract data ## Setup diff --git a/src/tools/AGENTS.md b/src/tools/AGENTS.md index 2ea37de..97d779c 100644 --- a/src/tools/AGENTS.md +++ b/src/tools/AGENTS.md @@ -52,6 +52,7 @@ - `get_legal_intakes` - `get_legal_intake_by_id` - Analytics: + - `get_analytics_catalog` - `run_analytics_query` ## Registry Invariants diff --git a/src/tools/analytics/AGENTS.md b/src/tools/analytics/AGENTS.md index c211e80..e70331d 100644 --- a/src/tools/analytics/AGENTS.md +++ b/src/tools/analytics/AGENTS.md @@ -2,13 +2,15 @@ ## Read This When -- You are editing the analytics query tool, embedded catalog summary, or changing the upstream `POST /v2.1/public/analytics/query/` contract. +- You are editing the analytics catalog tool, analytics query tool, embedded catalog summary, or changing the upstream analytics public API contracts. ## Public Surface +- `get_analytics_catalog.ts` — MCP tool and handler; `GET` `/v2.1/public/analytics/catalog/` with optional query `search`. - `run_analytics_query.ts` — MCP tool and handler; forwards to SpotDraft with optional `user_scoped`. -- `analytics_query_docs.ts` — `buildRunAnalyticsQueryDescription()`: embedded OpenAPI-aligned schema summary (allowlisted tables, limits, errors). **Keep in sync** with `paths['/api/v2.1/public/analytics/query/'].post.description` in the regional `https:///api/docs/schema/` when the catalog copy changes. +- `analytics_query_docs.ts` — `buildRunAnalyticsQueryDescription()`: embedded OpenAPI-aligned schema summary (allowlisted tables, limits, errors), plus MCP-only guidance to call `get_analytics_catalog` for workspace schema discovery. **Keep the upstream-oriented paragraphs in sync** with `paths['/api/v2.1/public/analytics/query/'].post.description` in the regional `https:///api/docs/schema/` when the catalog copy changes. ## Upstream +- `GET` `/v2.1/public/analytics/catalog/` — tables and columns for analytics SQL; optional `search` query parameter (see `paths['/api/v2.1/public/analytics/catalog/'].get.description` in regional schema). - `POST` `/v2.1/public/analytics/query/` — read-only GoogleSQL; request body `query` (required), `user_scoped` (optional). diff --git a/src/tools/analytics/analytics_query_docs.ts b/src/tools/analytics/analytics_query_docs.ts index f70fc40..ac84c7b 100644 --- a/src/tools/analytics/analytics_query_docs.ts +++ b/src/tools/analytics/analytics_query_docs.ts @@ -20,7 +20,7 @@ This capability must be enabled for your workspace. If it is not, the API return By default, the **\`Contracts\`** table is limited to rows the authenticated user may access. Optional body field **\`user_scoped\`** or header **\`X-Spotdraft-Analytics-User-Scoped\`** (\`true\` / \`false\`, or \`1\` / \`0\`, \`yes\` / \`no\`) overrides that. When both are omitted, the default is **\`true\`** in this environment. **\`user_scoped: false\`** (or the header equivalent) allows workspace-wide contract rows only for users with the workspace **Admin** role; others receive **403** with \`SD_PUBLIC_00044\`. **Tables and columns** -Use **bare catalog table names** (e.g. \`Contracts\`, \`Users\`) and **snake_case** public column names from the table catalog (e.g. \`execution_time\`, \`workflow_id\`). Only allowlisted tables and columns are accepted. +Use **bare catalog table names** (e.g. \`Contracts\`, \`Users\`) and **snake_case** public column names from the table catalog (e.g. \`execution_time\`, \`workflow_id\`). Only allowlisted tables and columns are accepted. For this workspace’s live schema (and to avoid \`SD_PUBLIC_00041\` on bad table/column names), call the **\`get_analytics_catalog\`** MCP tool first; use optional \`search\` to filter tables or columns. **Allowlisted tables (quick reference)** @@ -35,7 +35,7 @@ Use **bare catalog table names** (e.g. \`Contracts\`, \`Users\`) and **snake_cas | \`Entities_setup\` | Legal entities | | \`Signatures_setups\` | Signatory configuration per workflow | -For full column lists, types, and join guidance, see the [SpotDraft public analytics table catalog](${ANALYTICS_TABLE_CATALOG_URL}). +For full column lists, types, and join guidance, prefer **\`get_analytics_catalog\`**; the [SpotDraft public analytics table catalog](${ANALYTICS_TABLE_CATALOG_URL}) is supplementary documentation. **Limits (this deployment)** - At most **5,000** rows per response; \`truncated\` is **true** when more rows would have matched. @@ -61,5 +61,7 @@ export function buildRunAnalyticsQueryDescription(): string { **When to use:** Custom reporting, metrics, or joins across allowlisted analytics tables when contracts workspace analytics is enabled for your org. +**Before writing SQL:** Call **\`get_analytics_catalog\`** when you need accurate table and column names, types, or descriptions for this workspace, or when exploring what is queryable. Use it before \`run_analytics_query\` whenever the schema is unclear. + ${ANALYTICS_QUERY_OPERATION_SUMMARY}`; } diff --git a/src/tools/analytics/get_analytics_catalog.ts b/src/tools/analytics/get_analytics_catalog.ts new file mode 100644 index 0000000..10fae39 --- /dev/null +++ b/src/tools/analytics/get_analytics_catalog.ts @@ -0,0 +1,33 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { SpotDraftClient } from '../../spotdraft_client.js'; + +const getAnalyticsCatalogTool: Tool = { + name: 'get_analytics_catalog', + description: `Returns allowlisted analytics tables and column schemas for GoogleSQL used with run_analytics_query. Requires Insights view (INSIGHTS_VIEW). Optional search narrows results (case-insensitive match on table/column names and descriptions). Analytics query API must be enabled for the workspace or upstream returns 501.`, + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: + 'Optional filter: case-insensitive substring match on table names, table descriptions, column names, and column descriptions.', + }, + }, + }, +}; + +export interface GetAnalyticsCatalogRequest { + search?: string; +} + +const getAnalyticsCatalog = async ( + request: GetAnalyticsCatalogRequest, + spotdraftClient: SpotDraftClient, +): Promise => { + const endpoint = '/v2.1/public/analytics/catalog/'; + const search = request.search?.trim(); + const queryParams = search !== undefined && search !== '' ? new URLSearchParams({ search }) : undefined; + return spotdraftClient.get(endpoint, queryParams); +}; + +export { getAnalyticsCatalog, getAnalyticsCatalogTool }; diff --git a/src/tools/index.ts b/src/tools/index.ts index 8ea082b..c13b2c0 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -50,6 +50,7 @@ import { getTemplateMetadata, getTemplateMetadataTool } from './templates/get_te import { getTemplates, getTemplatesTool } from './templates/get_templates.js'; // Analytics tools +import { getAnalyticsCatalog, getAnalyticsCatalogTool } from './analytics/get_analytics_catalog.js'; import { runAnalyticsQuery, runAnalyticsQueryTool } from './analytics/run_analytics_query.js'; /** @@ -162,6 +163,11 @@ toolRegistry.register({ handler: withSpotDraftClient(getLegalIntakeById), }); +toolRegistry.register({ + tool: getAnalyticsCatalogTool, + handler: withSpotDraftClient(getAnalyticsCatalog), +}); + toolRegistry.register({ tool: runAnalyticsQueryTool, handler: withSpotDraftClient(runAnalyticsQuery),