-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add analytics catalog and query tools #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # AGENTS.md | ||
|
|
||
| ## Read This When | ||
|
|
||
| - 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), 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://<host>/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). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * 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. 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)** | ||
|
|
||
| | 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, 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. | ||
| - 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. | ||
|
|
||
| **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}`; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<unknown> => { | ||
| 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); | ||
|
Comment on lines
+23
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Finding type: Want Baz to fix this for you? Activate Fixer Prompt for AI Agents: |
||
| }; | ||
|
|
||
| export { getAnalyticsCatalog, getAnalyticsCatalogTool }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<unknown> => { | ||
| const query = request.query?.trim() ?? ''; | ||
| if (query === '') { | ||
| throw new ValidationError('query is required and cannot be empty.', { field: 'query' }); | ||
|
Comment on lines
+34
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Finding type: Want Baz to fix this for you? Activate Fixer Prompt for AI Agents: |
||
| } | ||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Finding type: Want Baz to fix this for you? Activate Fixer Prompt for AI Agents: |
||
| }; | ||
|
|
||
| export { runAnalyticsQuery, runAnalyticsQueryTool }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
location/requestUrlare passed verbatim intoAPIErrorandMCPError.toJSON()serializescontextunchanged, should we omit or redact them from client-facing error context?Finding type:
Basic Security Patterns| Severity: 🔴 HighWant Baz to fix this for you? Activate Fixer
Prompt for AI Agents: