Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Repository Notes

## DCO (Developer Certificate of Origin)

This repository requires DCO sign-off on all commits. Do **not** create commits unless you can sign them (i.e. use `git commit -s` or `--signoff`). Unsigned commits will be rejected.
20 changes: 20 additions & 0 deletions api-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,16 @@ paths:
required: false
schema:
$ref: "#/components/schemas/VersionIdPattern"
- name: sourceParameters
in: query
description: >-
JSON-encoded object of source parameter key-value pairs.
Keys must match parameter names declared on sources in the notebook's
Malloy model. Values can be strings, numbers, or booleans and are
converted to the correct Malloy literal based on declared parameter types.
required: false
schema:
type: string
responses:
"200":
description: Cell execution result
Expand Down Expand Up @@ -1916,6 +1926,16 @@ components:
type: boolean
default: false
description: 'If true, returns a simple JSON array of row objects in the form {"columnName": value}. If false (default), returns the full Malloy result with type metadata for rendering.'
sourceParameters:
type: object
additionalProperties: true
description: |
Source parameter values as key-value pairs. Keys must match parameter
names declared on the source in the Malloy model. Values can be strings,
numbers, or booleans — they are converted to the correct Malloy literal
based on the parameter's declared type. Required parameters (those without
a default value in the model) must be provided or the request will fail
with a 400 error.
versionId:
type: string
description: Version ID
Expand Down
37 changes: 37 additions & 0 deletions docs/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ The primary way clients interact with the server is through tool calls. These ar
* **Query Execution Tool**: Used by the AI to get data.
* `malloy_executeQuery`: Executes a Malloy query and returns the results in JSON format.

#### Source Parameters

Malloy sources can declare **parameters** that must (or may) be provided at query time. The `malloy_executeQuery` tool accepts an optional `sourceParameters` field — a JSON object of key-value string pairs — that supplies values for these parameters.

```json
{
"projectName": "my_project",
"packageName": "my_package",
"modelPath": "model.malloy",
"sourceName": "flights",
"queryName": "by_carrier",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought you were getting rid of this version of execute query.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to, but didn't think this PR was the right place. Happy to do this in another change though

"sourceParameters": {
"min_distance": "2000",
"carrier_filter": "UA"
}
}
```

**Type parsing:** Values are always passed as strings and the publisher parses them according to the parameter's declared type in the Malloy model:

| Malloy Type | Example Value | Notes |
|-------------|---------------|-------|
| `string` | `"UA"` | |
| `number` | `"2000"` | Integer or decimal |
| `boolean` | `"true"` | Accepts `true`/`false` or `1`/`0` |
| `date` | `"2024-01-15"` | ISO date format |
| `timestamp` | `"2024-01-15 10:30:00"` | ISO datetime format |
| `filter` | `"last week for two days"` | Malloy filter expression |

**Required parameters:** If a source declares a parameter without a default value, it must be provided in `sourceParameters` or the request will fail with a `400` error listing the missing parameter(s).

The same `sourceParameters` mechanism is available on the REST API's query execution endpoint (`POST .../models/{path}/query`) as a field in the JSON request body.

##### Implicit Parameters

Some models use parameters that are set by the system rather than by users or agents. By convention, these parameters are named with an `implicit_` prefix (e.g., `implicit_dataset_id`, `implicit_acl_token`). Agents should **not** set parameters whose names start with `implicit_` — they are managed by the deployment environment and injected by infrastructure (e.g., derived from authentication tokens or deployment configuration).

#### Prompts & Resources

The MCP server can also provide clients with **prompts** (e.g., suggested questions to start a conversation) and **resources** (e.g., links to documentation or data dictionaries). However, these are nascent capabilities of the MCP standard, and many current MCP clients do not yet utilize them.
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/controller/model.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class ModelController {
packageName: string,
notebookPath: string,
cellIndex: number,
sourceParameters?: Record<string, unknown>,
): Promise<{
type: "code" | "markdown";
text: string;
Expand All @@ -101,6 +102,6 @@ export class ModelController {
throw new ModelNotFoundError(`${notebookPath} is a model`);
}

return model.executeNotebookCell(cellIndex);
return model.executeNotebookCell(cellIndex, sourceParameters);
}
}
2 changes: 2 additions & 0 deletions packages/server/src/controller/query.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class QueryController {
queryName: string,
query: string,
compactJson: boolean = false,
sourceParameters?: Record<string, unknown>,
): Promise<ApiQuery> {
const project = await this.projectStore.getProject(projectName, false);
const p = await project.getPackage(packageName, false);
Expand All @@ -41,6 +42,7 @@ export class QueryController {
sourceName,
queryName,
query,
sourceParameters,
);
const renderLogs = validateRenderTags(result);
return {
Expand Down
12 changes: 12 additions & 0 deletions packages/server/src/mcp/tools/execute_query_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ const executeQueryShape = {
query: z.string().optional().describe("Ad-hoc Malloy query code"),
sourceName: z.string().optional().describe("Source name for a view"),
queryName: z.string().optional().describe("Named query or view"),
sourceParameters: z
.record(z.unknown())
.optional()
.describe(
"Source parameter values as key-value pairs. " +
"Values can be strings, numbers, or booleans — they are converted " +
"to the correct Malloy literal based on the parameter's declared type. " +
"Required parameters that lack a default value must be provided.",
),
};

// Type inference is handled automatically by the MCP server based on the executeQueryShape
Expand All @@ -49,6 +58,7 @@ export function registerExecuteQueryTool(
query,
sourceName,
queryName,
sourceParameters,
} = params;

logger.info("[MCP Tool executeQuery] Received params:", { params });
Expand Down Expand Up @@ -120,6 +130,7 @@ export function registerExecuteQueryTool(
undefined,
undefined,
query,
sourceParameters,
);
const { validateRenderTags } = await import(
"@malloydata/render-validator"
Expand Down Expand Up @@ -165,6 +176,7 @@ export function registerExecuteQueryTool(
sourceName,
queryName,
undefined,
sourceParameters,
);
const { validateRenderTags } = await import(
"@malloydata/render-validator"
Expand Down
13 changes: 13 additions & 0 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,12 +817,21 @@ app.get(
// Express stores wildcard matches in params['0']
const notebookPath = (req.params as Record<string, string>)["0"];

const sourceParameters =
typeof req.query.sourceParameters === "string"
? (JSON.parse(req.query.sourceParameters) as Record<
string,
unknown
>)
: undefined;

res.status(200).json(
await modelController.executeNotebookCell(
req.params.projectName,
req.params.packageName,
notebookPath,
cellIndex,
sourceParameters,
),
);
} catch (error) {
Expand Down Expand Up @@ -870,6 +879,9 @@ app.post(
try {
// Express stores wildcard matches in params['0']
const modelPath = (req.params as Record<string, string>)["0"];
const sourceParameters = req.body.sourceParameters as
| Record<string, unknown>
| undefined;
res.status(200).json(
await queryController.getQuery(
req.params.projectName,
Expand All @@ -879,6 +891,7 @@ app.post(
req.body.queryName as string,
req.body.query as string,
req.body.compactJson === true,
sourceParameters,
),
);
} catch (error) {
Expand Down
Loading
Loading