@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.
Files changed (59) hide show
  1. package/dist/app/api-doc.yaml +84 -66
  2. package/dist/app/assets/{EnvironmentPage-BScgHmkw.js → EnvironmentPage-BRMCY9d8.js} +1 -1
  3. package/dist/app/assets/HomePage-DQzgkiMI.js +1 -0
  4. package/dist/app/assets/{MainPage-DWfF4jXW.js → MainPage-sZdUjUcu.js} +2 -2
  5. package/dist/app/assets/MaterializationsPage-C_jQQUCJ.js +1 -0
  6. package/dist/app/assets/{ModelPage-BiNOgK_e.js → ModelPage-Bh62OIEq.js} +1 -1
  7. package/dist/app/assets/{PackagePage-DAN5V7gu.js → PackagePage-DJW4xjLp.js} +1 -1
  8. package/dist/app/assets/{RouteError-CEnIzuKN.js → RouteError-Vi5yGs_F.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-gA1ceqHP.js → WorkbookPage-BvJMi21d.js} +1 -1
  10. package/dist/app/assets/{core-AOmIKwkc.es-Dclu1Fga.js → core-BbW0t3RQ.es-L-mZcOk3.js} +1 -1
  11. package/dist/app/assets/index-CCcHdeew.js +1760 -0
  12. package/dist/app/assets/index-CNFX-CGL.js +40 -0
  13. package/dist/app/assets/{index-DtlPzNxc.js → index-DuqTjxM_.js} +114 -114
  14. package/dist/app/assets/index.umd-GgEb4WfT.js +2467 -0
  15. package/dist/app/index.html +1 -1
  16. package/dist/server.mjs +26182 -16839
  17. package/package.json +14 -11
  18. package/src/controller/materialization.controller.spec.ts +125 -0
  19. package/src/controller/materialization.controller.ts +23 -27
  20. package/src/health.ts +6 -0
  21. package/src/materialization_metrics.ts +117 -34
  22. package/src/mcp/agent_server.protocol.spec.ts +78 -0
  23. package/src/mcp/agent_server.spec.ts +18 -0
  24. package/src/mcp/agent_server.ts +144 -0
  25. package/src/mcp/server.ts +3 -0
  26. package/src/mcp/skills/build_skills_bundle.spec.ts +51 -0
  27. package/src/mcp/skills/build_skills_bundle.ts +66 -0
  28. package/src/mcp/skills/skills_bundle.json +1 -0
  29. package/src/mcp/skills/skills_bundle.spec.ts +33 -0
  30. package/src/mcp/tools/docs_search/build_docs_index.ts +132 -0
  31. package/src/mcp/tools/docs_search/malloy_docs_index.json +1 -0
  32. package/src/mcp/tools/docs_search_tool.spec.ts +32 -0
  33. package/src/mcp/tools/docs_search_tool.ts +138 -0
  34. package/src/mcp/tools/get_context_eval.ts +126 -0
  35. package/src/mcp/tools/get_context_tool.spec.ts +44 -0
  36. package/src/mcp/tools/get_context_tool.ts +341 -0
  37. package/src/server-old.ts +2 -11
  38. package/src/server.ts +16 -10
  39. package/src/service/build_plan.spec.ts +116 -0
  40. package/src/service/build_plan.ts +238 -0
  41. package/src/service/connection.ts +4 -0
  42. package/src/service/connection_config.spec.ts +182 -1
  43. package/src/service/connection_config.ts +70 -0
  44. package/src/service/db_utils.spec.ts +159 -1
  45. package/src/service/db_utils.ts +131 -0
  46. package/src/service/materialization_service.spec.ts +388 -184
  47. package/src/service/materialization_service.ts +156 -442
  48. package/src/service/materialization_test_fixtures.ts +119 -0
  49. package/src/service/package.ts +41 -1
  50. package/src/storage/DatabaseInterface.ts +5 -13
  51. package/src/storage/duckdb/MaterializationRepository.ts +5 -14
  52. package/src/storage/duckdb/schema.ts +4 -5
  53. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +72 -211
  54. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +6 -0
  55. package/dist/app/assets/HomePage-CGedji_w.js +0 -1
  56. package/dist/app/assets/MaterializationsPage-B9PDlk8c.js +0 -1
  57. package/dist/app/assets/index-DGGe8UpP.js +0 -40
  58. package/dist/app/assets/index-uu6UpHd2.js +0 -1812
  59. 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
+ }