@malloy-publisher/server 0.0.207 → 0.0.209
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.
- package/dist/app/api-doc.yaml +84 -66
- package/dist/app/assets/{EnvironmentPage-BScgHmkw.js → EnvironmentPage-BRMCY9d8.js} +1 -1
- package/dist/app/assets/HomePage-DQzgkiMI.js +1 -0
- package/dist/app/assets/{MainPage-DWfF4jXW.js → MainPage-sZdUjUcu.js} +2 -2
- package/dist/app/assets/MaterializationsPage-C_jQQUCJ.js +1 -0
- package/dist/app/assets/{ModelPage-BiNOgK_e.js → ModelPage-Bh62OIEq.js} +1 -1
- package/dist/app/assets/{PackagePage-DAN5V7gu.js → PackagePage-DJW4xjLp.js} +1 -1
- package/dist/app/assets/{RouteError-CEnIzuKN.js → RouteError-Vi5yGs_F.js} +1 -1
- package/dist/app/assets/{WorkbookPage-gA1ceqHP.js → WorkbookPage-BvJMi21d.js} +1 -1
- package/dist/app/assets/{core-AOmIKwkc.es-Dclu1Fga.js → core-BbW0t3RQ.es-L-mZcOk3.js} +1 -1
- package/dist/app/assets/index-CCcHdeew.js +1760 -0
- package/dist/app/assets/index-CNFX-CGL.js +40 -0
- package/dist/app/assets/{index-DtlPzNxc.js → index-DuqTjxM_.js} +114 -114
- package/dist/app/assets/index.umd-GgEb4WfT.js +2467 -0
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +26182 -16839
- package/package.json +14 -11
- package/src/controller/materialization.controller.spec.ts +125 -0
- package/src/controller/materialization.controller.ts +23 -27
- package/src/health.ts +6 -0
- package/src/materialization_metrics.ts +117 -34
- package/src/mcp/agent_server.protocol.spec.ts +78 -0
- package/src/mcp/agent_server.spec.ts +18 -0
- package/src/mcp/agent_server.ts +144 -0
- package/src/mcp/server.ts +3 -0
- package/src/mcp/skills/build_skills_bundle.spec.ts +51 -0
- package/src/mcp/skills/build_skills_bundle.ts +66 -0
- package/src/mcp/skills/skills_bundle.json +1 -0
- package/src/mcp/skills/skills_bundle.spec.ts +33 -0
- package/src/mcp/tools/docs_search/build_docs_index.ts +132 -0
- package/src/mcp/tools/docs_search/malloy_docs_index.json +1 -0
- package/src/mcp/tools/docs_search_tool.spec.ts +32 -0
- package/src/mcp/tools/docs_search_tool.ts +138 -0
- package/src/mcp/tools/get_context_eval.ts +126 -0
- package/src/mcp/tools/get_context_tool.spec.ts +44 -0
- package/src/mcp/tools/get_context_tool.ts +341 -0
- package/src/server-old.ts +2 -11
- package/src/server.ts +16 -10
- package/src/service/build_plan.spec.ts +116 -0
- package/src/service/build_plan.ts +238 -0
- package/src/service/connection.ts +4 -0
- package/src/service/connection_config.spec.ts +182 -1
- package/src/service/connection_config.ts +70 -0
- package/src/service/db_utils.spec.ts +159 -1
- package/src/service/db_utils.ts +131 -0
- package/src/service/materialization_service.spec.ts +388 -184
- package/src/service/materialization_service.ts +156 -442
- package/src/service/materialization_test_fixtures.ts +119 -0
- package/src/service/package.ts +41 -1
- package/src/storage/DatabaseInterface.ts +5 -13
- package/src/storage/duckdb/MaterializationRepository.ts +5 -14
- package/src/storage/duckdb/schema.ts +4 -5
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +72 -211
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +6 -0
- package/dist/app/assets/HomePage-CGedji_w.js +0 -1
- package/dist/app/assets/MaterializationsPage-B9PDlk8c.js +0 -1
- package/dist/app/assets/index-DGGe8UpP.js +0 -40
- package/dist/app/assets/index-uu6UpHd2.js +0 -1812
- package/dist/app/assets/index.umd-DDq93YX4.js +0 -2469
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import * as http from "http";
|
|
6
|
+
import { registerHealthEndpoints } from "../health";
|
|
7
|
+
import { logger } from "../logger";
|
|
8
|
+
import { EnvironmentStore } from "../service/environment_store";
|
|
9
|
+
import { registerDocsSearchTool } from "./tools/docs_search_tool";
|
|
10
|
+
import { registerGetContextTool } from "./tools/get_context_tool";
|
|
11
|
+
import skillsBundle from "./skills/skills_bundle.json";
|
|
12
|
+
|
|
13
|
+
const AGENT_MCP_ENDPOINT = "/mcp";
|
|
14
|
+
|
|
15
|
+
// Build-time bundle of the agent skills (see skills/build_skills_bundle.ts), exposed
|
|
16
|
+
// as MCP prompts for the dual-channel delivery below.
|
|
17
|
+
const AGENT_SKILLS = (
|
|
18
|
+
skillsBundle as {
|
|
19
|
+
skills: { name: string; description: string; body: string }[];
|
|
20
|
+
}
|
|
21
|
+
).skills;
|
|
22
|
+
|
|
23
|
+
export const agentServerInfo = {
|
|
24
|
+
name: "malloy-publisher-agent-mcp-server",
|
|
25
|
+
version: "0.0.1",
|
|
26
|
+
displayName: "Malloy Publisher Agent MCP Server",
|
|
27
|
+
description:
|
|
28
|
+
"Agent retrieval tools (semantic model context and docs search) for Malloy, served on a separate MCP endpoint from the core Publisher MCP tools.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds an MCP server exposing ONLY the agent retrieval tools (malloy_getContext,
|
|
33
|
+
* malloy_searchDocs). Kept deliberately separate from the core initializeMcpServer so
|
|
34
|
+
* these tools and their tests stay isolated from the existing MCP tool surface.
|
|
35
|
+
*/
|
|
36
|
+
export function initializeAgentMcpServer(
|
|
37
|
+
environmentStore: EnvironmentStore,
|
|
38
|
+
): McpServer {
|
|
39
|
+
const mcpServer = new McpServer(agentServerInfo);
|
|
40
|
+
registerGetContextTool(mcpServer, environmentStore);
|
|
41
|
+
registerDocsSearchTool(mcpServer, environmentStore);
|
|
42
|
+
|
|
43
|
+
// Dual-channel: also expose each skill as an MCP prompt, so hosts that ingest MCP
|
|
44
|
+
// but do not load skill files (e.g. Codex, ChatGPT, Cursor) can pull the same
|
|
45
|
+
// guidance. Skill-aware hosts (Claude Code/Desktop) use the native skill files.
|
|
46
|
+
// Note: MCP prompts are on-demand, so always-on "prevention" skills become
|
|
47
|
+
// on-demand here; see docs/agent-skills/design-principles.md.
|
|
48
|
+
for (const skill of AGENT_SKILLS) {
|
|
49
|
+
mcpServer.prompt(skill.name, skill.description, () => ({
|
|
50
|
+
messages: [
|
|
51
|
+
{
|
|
52
|
+
role: "user" as const,
|
|
53
|
+
content: { type: "text" as const, text: skill.body },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return mcpServer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Starts the agent MCP server on its own HTTP listener (a third Publisher listener),
|
|
64
|
+
* using the same stateless StreamableHTTP transport pattern as the core MCP server.
|
|
65
|
+
* Returns the http.Server so the caller can manage shutdown.
|
|
66
|
+
*/
|
|
67
|
+
export function startAgentMcpServer(
|
|
68
|
+
environmentStore: EnvironmentStore,
|
|
69
|
+
host: string,
|
|
70
|
+
port: number,
|
|
71
|
+
): http.Server {
|
|
72
|
+
const agentMcpApp = express();
|
|
73
|
+
registerHealthEndpoints(agentMcpApp);
|
|
74
|
+
agentMcpApp.use(AGENT_MCP_ENDPOINT, express.json());
|
|
75
|
+
agentMcpApp.use(AGENT_MCP_ENDPOINT, cors());
|
|
76
|
+
|
|
77
|
+
agentMcpApp.all(AGENT_MCP_ENDPOINT, async (req, res) => {
|
|
78
|
+
if (req.method !== "POST") {
|
|
79
|
+
res.setHeader("Allow", "POST");
|
|
80
|
+
res.status(405).json({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
error: {
|
|
83
|
+
code: -32601,
|
|
84
|
+
message: "Method Not Allowed in Stateless Mode",
|
|
85
|
+
},
|
|
86
|
+
id: null,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const transport = new StreamableHTTPServerTransport({
|
|
93
|
+
sessionIdGenerator: undefined,
|
|
94
|
+
});
|
|
95
|
+
transport.onerror = (err: Error) => {
|
|
96
|
+
logger.error("[Agent MCP Transport Error]", {
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const requestMcpServer = initializeAgentMcpServer(environmentStore);
|
|
101
|
+
await requestMcpServer.connect(transport);
|
|
102
|
+
res.on("close", () => {
|
|
103
|
+
transport.close().catch((err) => {
|
|
104
|
+
logger.error("[Agent MCP Transport Error] close failed", {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
await transport.handleRequest(req, res, req.body);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logger.error("[Agent MCP] request handler error", {
|
|
112
|
+
error: err instanceof Error ? err.message : String(err),
|
|
113
|
+
});
|
|
114
|
+
if (!res.headersSent) {
|
|
115
|
+
res.status(500).json({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
error: { code: -32603, message: "Internal server error" },
|
|
118
|
+
id: null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const agentMcpServer = agentMcpApp.listen(port, host, () => {
|
|
125
|
+
logger.info(
|
|
126
|
+
`Agent MCP server listening at http://${host}:${port}${AGENT_MCP_ENDPOINT}`,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// The agent MCP server is auxiliary: a bind failure here (e.g. EADDRINUSE)
|
|
131
|
+
// must not take down the main API or the core MCP server. Without this
|
|
132
|
+
// handler the listener's 'error' event would surface as an uncaught
|
|
133
|
+
// exception and crash the whole process at boot. Log and degrade instead.
|
|
134
|
+
agentMcpServer.on("error", (err: NodeJS.ErrnoException) => {
|
|
135
|
+
logger.error("Agent MCP server failed to start; continuing without it", {
|
|
136
|
+
host,
|
|
137
|
+
port,
|
|
138
|
+
code: err.code,
|
|
139
|
+
error: err.message,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return agentMcpServer;
|
|
144
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -52,6 +52,9 @@ export function initializeMcpServer(
|
|
|
52
52
|
|
|
53
53
|
registerTools(mcpServer, environmentStore);
|
|
54
54
|
|
|
55
|
+
// Note: malloy_getContext and malloy_searchDocs are intentionally NOT registered
|
|
56
|
+
// here. They run on a separate, isolated agent MCP server (see mcp/agent_server.ts).
|
|
57
|
+
|
|
55
58
|
logger.info("[MCP Init] Registering prompt capability...");
|
|
56
59
|
registerPromptCapability(mcpServer, environmentStore);
|
|
57
60
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseSkill, unquote } from "./build_skills_bundle";
|
|
3
|
+
|
|
4
|
+
describe("build_skills_bundle parseSkill", () => {
|
|
5
|
+
it("parses name, description, and body from frontmatter", () => {
|
|
6
|
+
const md =
|
|
7
|
+
"---\nname: malloy-queries\ndescription: Query patterns.\n---\n# Body\n\nText.";
|
|
8
|
+
const s = parseSkill(md, "dir");
|
|
9
|
+
expect(s.name).toBe("malloy-queries");
|
|
10
|
+
expect(s.description).toBe("Query patterns.");
|
|
11
|
+
expect(s.body).toBe("# Body\n\nText.");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("keeps colons in the description (line-based, not split on colon)", () => {
|
|
15
|
+
const md =
|
|
16
|
+
"---\nname: x\ndescription: Use when: a thing happens.\n---\nbody";
|
|
17
|
+
expect(parseSkill(md, "dir").description).toBe(
|
|
18
|
+
"Use when: a thing happens.",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("tolerates CRLF line endings", () => {
|
|
23
|
+
const md = "---\r\nname: x\r\ndescription: d\r\n---\r\nbody line";
|
|
24
|
+
const s = parseSkill(md, "dir");
|
|
25
|
+
expect(s.name).toBe("x");
|
|
26
|
+
expect(s.description).toBe("d");
|
|
27
|
+
expect(s.body).toBe("body line");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("falls back to the dir name and empty description when frontmatter is missing", () => {
|
|
31
|
+
const s = parseSkill("# Just a body, no frontmatter", "fallback-name");
|
|
32
|
+
expect(s.name).toBe("fallback-name");
|
|
33
|
+
expect(s.description).toBe("");
|
|
34
|
+
expect(s.body).toBe("# Just a body, no frontmatter");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("strips surrounding quotes from values", () => {
|
|
38
|
+
const md = "---\nname: \"quoted-name\"\ndescription: 'q'\n---\nb";
|
|
39
|
+
const s = parseSkill(md, "dir");
|
|
40
|
+
expect(s.name).toBe("quoted-name");
|
|
41
|
+
expect(s.description).toBe("q");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("build_skills_bundle unquote", () => {
|
|
46
|
+
it("removes a single layer of surrounding quotes", () => {
|
|
47
|
+
expect(unquote('"hi"')).toBe("hi");
|
|
48
|
+
expect(unquote("'hi'")).toBe("hi");
|
|
49
|
+
expect(unquote("plain")).toBe("plain");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a bundled JSON of the agent skills so the agent MCP server can expose them as
|
|
3
|
+
* MCP prompts (dual-channel): skill-aware hosts read the skill files directly, while
|
|
4
|
+
* hosts that only ingest MCP get the same guidance through these prompts.
|
|
5
|
+
*
|
|
6
|
+
* Committed as a build-time asset so the server has no runtime dependency on the
|
|
7
|
+
* skills directory layout. Regenerate when the skills change:
|
|
8
|
+
*
|
|
9
|
+
* bun run packages/server/src/mcp/skills/build_skills_bundle.ts <skills-dir> [out.json]
|
|
10
|
+
*
|
|
11
|
+
* <skills-dir> points at the repo's top-level skills/ directory.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
interface SkillEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
body: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function unquote(s: string): string {
|
|
23
|
+
return s.trim().replace(/^["']|["']$/g, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse a SKILL.md into its frontmatter name/description and Markdown body. */
|
|
27
|
+
export function parseSkill(md: string, dirName: string): SkillEntry {
|
|
28
|
+
const text = md.replace(/\r\n/g, "\n"); // tolerate CRLF-committed files
|
|
29
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
30
|
+
const front = fm ? fm[1] : "";
|
|
31
|
+
const body = (fm ? fm[2] : text).trim();
|
|
32
|
+
const name = unquote(front.match(/^name:\s*(.+)$/m)?.[1] ?? dirName);
|
|
33
|
+
const description = unquote(
|
|
34
|
+
front.match(/^description:\s*(.+)$/m)?.[1] ?? "",
|
|
35
|
+
);
|
|
36
|
+
return { name, description, body };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main(): void {
|
|
40
|
+
const skillsDir = process.argv[2];
|
|
41
|
+
const outFile =
|
|
42
|
+
process.argv[3] || path.join(import.meta.dir, "skills_bundle.json");
|
|
43
|
+
if (!skillsDir || !fs.existsSync(skillsDir)) {
|
|
44
|
+
console.error(
|
|
45
|
+
`skills dir not found: ${skillsDir}\nUsage: bun run build_skills_bundle.ts <skills-dir> [out.json]`,
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const skills: SkillEntry[] = [];
|
|
51
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
|
|
54
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
55
|
+
skills.push(parseSkill(fs.readFileSync(skillFile, "utf8"), entry.name));
|
|
56
|
+
}
|
|
57
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(outFile, JSON.stringify({ skills }));
|
|
60
|
+
console.log(`Wrote ${skills.length} skills to ${outFile}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Only run when invoked directly (bun run ...), not when imported by a test.
|
|
64
|
+
if (import.meta.main) {
|
|
65
|
+
main();
|
|
66
|
+
}
|