@sammorrowdrums/mcpi-ext 0.1.0

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 (213) hide show
  1. package/.dockerignore +6 -0
  2. package/.github/dependabot.yml +25 -0
  3. package/.github/workflows/ci.yml +50 -0
  4. package/.hadolint.yaml +6 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +7 -0
  7. package/AGENTS.md +118 -0
  8. package/DECISIONS.md +99 -0
  9. package/Dockerfile +39 -0
  10. package/README.md +278 -0
  11. package/aube-lock.yaml +4146 -0
  12. package/dist/code-mode/code-mode-integration.test.d.ts +2 -0
  13. package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
  14. package/dist/code-mode/code-mode-integration.test.js +116 -0
  15. package/dist/code-mode/code-mode-integration.test.js.map +1 -0
  16. package/dist/code-mode/eligibility.d.ts +10 -0
  17. package/dist/code-mode/eligibility.d.ts.map +1 -0
  18. package/dist/code-mode/eligibility.js +14 -0
  19. package/dist/code-mode/eligibility.js.map +1 -0
  20. package/dist/code-mode/eligibility.test.d.ts +2 -0
  21. package/dist/code-mode/eligibility.test.d.ts.map +1 -0
  22. package/dist/code-mode/eligibility.test.js +53 -0
  23. package/dist/code-mode/eligibility.test.js.map +1 -0
  24. package/dist/code-mode/executor.d.ts +35 -0
  25. package/dist/code-mode/executor.d.ts.map +1 -0
  26. package/dist/code-mode/executor.js +120 -0
  27. package/dist/code-mode/executor.js.map +1 -0
  28. package/dist/code-mode/executor.test.d.ts +2 -0
  29. package/dist/code-mode/executor.test.d.ts.map +1 -0
  30. package/dist/code-mode/executor.test.js +130 -0
  31. package/dist/code-mode/executor.test.js.map +1 -0
  32. package/dist/code-mode/index.d.ts +65 -0
  33. package/dist/code-mode/index.d.ts.map +1 -0
  34. package/dist/code-mode/index.js +175 -0
  35. package/dist/code-mode/index.js.map +1 -0
  36. package/dist/code-mode/tools.d.ts +45 -0
  37. package/dist/code-mode/tools.d.ts.map +1 -0
  38. package/dist/code-mode/tools.js +70 -0
  39. package/dist/code-mode/tools.js.map +1 -0
  40. package/dist/code-mode/type-hints.d.ts +20 -0
  41. package/dist/code-mode/type-hints.d.ts.map +1 -0
  42. package/dist/code-mode/type-hints.js +189 -0
  43. package/dist/code-mode/type-hints.js.map +1 -0
  44. package/dist/code-mode/type-hints.test.d.ts +2 -0
  45. package/dist/code-mode/type-hints.test.d.ts.map +1 -0
  46. package/dist/code-mode/type-hints.test.js +157 -0
  47. package/dist/code-mode/type-hints.test.js.map +1 -0
  48. package/dist/docker-e2e.d.ts +55 -0
  49. package/dist/docker-e2e.d.ts.map +1 -0
  50. package/dist/docker-e2e.js +235 -0
  51. package/dist/docker-e2e.js.map +1 -0
  52. package/dist/docker-e2e.test.d.ts +2 -0
  53. package/dist/docker-e2e.test.d.ts.map +1 -0
  54. package/dist/docker-e2e.test.js +176 -0
  55. package/dist/docker-e2e.test.js.map +1 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +128 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/index.test.d.ts +2 -0
  61. package/dist/index.test.d.ts.map +1 -0
  62. package/dist/index.test.js +8 -0
  63. package/dist/index.test.js.map +1 -0
  64. package/dist/mcp/client-manager.d.ts +46 -0
  65. package/dist/mcp/client-manager.d.ts.map +1 -0
  66. package/dist/mcp/client-manager.js +148 -0
  67. package/dist/mcp/client-manager.js.map +1 -0
  68. package/dist/mcp/client-manager.test.d.ts +2 -0
  69. package/dist/mcp/client-manager.test.d.ts.map +1 -0
  70. package/dist/mcp/client-manager.test.js +204 -0
  71. package/dist/mcp/client-manager.test.js.map +1 -0
  72. package/dist/mcp/config-loader.d.ts +8 -0
  73. package/dist/mcp/config-loader.d.ts.map +1 -0
  74. package/dist/mcp/config-loader.js +43 -0
  75. package/dist/mcp/config-loader.js.map +1 -0
  76. package/dist/mcp/config-loader.test.d.ts +2 -0
  77. package/dist/mcp/config-loader.test.d.ts.map +1 -0
  78. package/dist/mcp/config-loader.test.js +93 -0
  79. package/dist/mcp/config-loader.test.js.map +1 -0
  80. package/dist/mcp/config.d.ts +46 -0
  81. package/dist/mcp/config.d.ts.map +1 -0
  82. package/dist/mcp/config.js +28 -0
  83. package/dist/mcp/config.js.map +1 -0
  84. package/dist/mcp/index.d.ts +5 -0
  85. package/dist/mcp/index.d.ts.map +1 -0
  86. package/dist/mcp/index.js +4 -0
  87. package/dist/mcp/index.js.map +1 -0
  88. package/dist/skills/discover.d.ts +10 -0
  89. package/dist/skills/discover.d.ts.map +1 -0
  90. package/dist/skills/discover.js +60 -0
  91. package/dist/skills/discover.js.map +1 -0
  92. package/dist/skills/format.d.ts +10 -0
  93. package/dist/skills/format.d.ts.map +1 -0
  94. package/dist/skills/format.js +38 -0
  95. package/dist/skills/format.js.map +1 -0
  96. package/dist/skills/format.test.d.ts +2 -0
  97. package/dist/skills/format.test.d.ts.map +1 -0
  98. package/dist/skills/format.test.js +63 -0
  99. package/dist/skills/format.test.js.map +1 -0
  100. package/dist/skills/index.d.ts +6 -0
  101. package/dist/skills/index.d.ts.map +1 -0
  102. package/dist/skills/index.js +6 -0
  103. package/dist/skills/index.js.map +1 -0
  104. package/dist/skills/load-skill-tool.d.ts +40 -0
  105. package/dist/skills/load-skill-tool.d.ts.map +1 -0
  106. package/dist/skills/load-skill-tool.js +104 -0
  107. package/dist/skills/load-skill-tool.js.map +1 -0
  108. package/dist/skills/mcp-tool-proxy.d.ts +11 -0
  109. package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
  110. package/dist/skills/mcp-tool-proxy.js +116 -0
  111. package/dist/skills/mcp-tool-proxy.js.map +1 -0
  112. package/dist/skills/skill-integration.test.d.ts +2 -0
  113. package/dist/skills/skill-integration.test.d.ts.map +1 -0
  114. package/dist/skills/skill-integration.test.js +119 -0
  115. package/dist/skills/skill-integration.test.js.map +1 -0
  116. package/dist/skills/skill-registry.d.ts +39 -0
  117. package/dist/skills/skill-registry.d.ts.map +1 -0
  118. package/dist/skills/skill-registry.js +48 -0
  119. package/dist/skills/skill-registry.js.map +1 -0
  120. package/dist/skills/skill-registry.test.d.ts +2 -0
  121. package/dist/skills/skill-registry.test.d.ts.map +1 -0
  122. package/dist/skills/skill-registry.test.js +67 -0
  123. package/dist/skills/skill-registry.test.js.map +1 -0
  124. package/dist/test-servers/weather-server.d.ts +12 -0
  125. package/dist/test-servers/weather-server.d.ts.map +1 -0
  126. package/dist/test-servers/weather-server.js +139 -0
  127. package/dist/test-servers/weather-server.js.map +1 -0
  128. package/dist/test-servers/weather-stdio.d.ts +2 -0
  129. package/dist/test-servers/weather-stdio.d.ts.map +1 -0
  130. package/dist/test-servers/weather-stdio.js +11 -0
  131. package/dist/test-servers/weather-stdio.js.map +1 -0
  132. package/dist/tool-cli/cli.d.ts +3 -0
  133. package/dist/tool-cli/cli.d.ts.map +1 -0
  134. package/dist/tool-cli/cli.js +179 -0
  135. package/dist/tool-cli/cli.js.map +1 -0
  136. package/dist/tool-cli/constants.d.ts +7 -0
  137. package/dist/tool-cli/constants.d.ts.map +1 -0
  138. package/dist/tool-cli/constants.js +15 -0
  139. package/dist/tool-cli/constants.js.map +1 -0
  140. package/dist/tool-cli/format.d.ts +8 -0
  141. package/dist/tool-cli/format.d.ts.map +1 -0
  142. package/dist/tool-cli/format.js +57 -0
  143. package/dist/tool-cli/format.js.map +1 -0
  144. package/dist/tool-cli/format.test.d.ts +2 -0
  145. package/dist/tool-cli/format.test.d.ts.map +1 -0
  146. package/dist/tool-cli/format.test.js +30 -0
  147. package/dist/tool-cli/format.test.js.map +1 -0
  148. package/dist/tool-cli/index.d.ts +4 -0
  149. package/dist/tool-cli/index.d.ts.map +1 -0
  150. package/dist/tool-cli/index.js +4 -0
  151. package/dist/tool-cli/index.js.map +1 -0
  152. package/dist/tool-cli/rpc-client.d.ts +6 -0
  153. package/dist/tool-cli/rpc-client.d.ts.map +1 -0
  154. package/dist/tool-cli/rpc-client.js +25 -0
  155. package/dist/tool-cli/rpc-client.js.map +1 -0
  156. package/dist/tool-cli/rpc-server.d.ts +48 -0
  157. package/dist/tool-cli/rpc-server.d.ts.map +1 -0
  158. package/dist/tool-cli/rpc-server.js +220 -0
  159. package/dist/tool-cli/rpc-server.js.map +1 -0
  160. package/dist/tool-cli/rpc-server.test.d.ts +2 -0
  161. package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
  162. package/dist/tool-cli/rpc-server.test.js +154 -0
  163. package/dist/tool-cli/rpc-server.test.js.map +1 -0
  164. package/eslint.config.js +27 -0
  165. package/images/banner.webp +0 -0
  166. package/images/code-c-maude.webp +0 -0
  167. package/images/nuclear-mcp-football.webp +0 -0
  168. package/images/the-skill-dealer.webp +0 -0
  169. package/mise.toml +65 -0
  170. package/package.json +46 -0
  171. package/scripts/docker-entrypoint.sh +29 -0
  172. package/scripts/test-echo-server.mjs +28 -0
  173. package/scripts/test-mcp-integration.mjs +59 -0
  174. package/scripts/test-tool-cli-smoke.mjs +62 -0
  175. package/src/code-mode/code-mode-integration.test.ts +135 -0
  176. package/src/code-mode/eligibility.test.ts +60 -0
  177. package/src/code-mode/eligibility.ts +16 -0
  178. package/src/code-mode/executor.test.ts +168 -0
  179. package/src/code-mode/executor.ts +163 -0
  180. package/src/code-mode/index.ts +208 -0
  181. package/src/code-mode/tools.ts +110 -0
  182. package/src/code-mode/type-hints.test.ts +187 -0
  183. package/src/code-mode/type-hints.ts +249 -0
  184. package/src/docker-e2e.test.ts +195 -0
  185. package/src/docker-e2e.ts +277 -0
  186. package/src/index.test.ts +8 -0
  187. package/src/index.ts +158 -0
  188. package/src/mcp/client-manager.test.ts +250 -0
  189. package/src/mcp/client-manager.ts +199 -0
  190. package/src/mcp/config-loader.test.ts +115 -0
  191. package/src/mcp/config-loader.ts +46 -0
  192. package/src/mcp/config.ts +40 -0
  193. package/src/mcp/index.ts +4 -0
  194. package/src/skills/discover.ts +81 -0
  195. package/src/skills/format.test.ts +70 -0
  196. package/src/skills/format.ts +42 -0
  197. package/src/skills/index.ts +9 -0
  198. package/src/skills/load-skill-tool.ts +136 -0
  199. package/src/skills/mcp-tool-proxy.ts +143 -0
  200. package/src/skills/skill-integration.test.ts +139 -0
  201. package/src/skills/skill-registry.test.ts +83 -0
  202. package/src/skills/skill-registry.ts +69 -0
  203. package/src/test-servers/weather-server.ts +171 -0
  204. package/src/test-servers/weather-stdio.ts +11 -0
  205. package/src/tool-cli/cli.ts +232 -0
  206. package/src/tool-cli/constants.ts +15 -0
  207. package/src/tool-cli/format.test.ts +33 -0
  208. package/src/tool-cli/format.ts +56 -0
  209. package/src/tool-cli/index.ts +3 -0
  210. package/src/tool-cli/rpc-client.ts +39 -0
  211. package/src/tool-cli/rpc-server.test.ts +193 -0
  212. package/src/tool-cli/rpc-server.ts +287 -0
  213. package/tsconfig.json +21 -0
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { McpClientManager } from "../mcp/client-manager.js";
4
+ import { SkillRegistry } from "./skill-registry.js";
5
+ import { discoverSkillsFromServer } from "./discover.js";
6
+ import { formatMcpSkillsForPrompt } from "./format.js";
7
+ import { createLoadSkillTool } from "./load-skill-tool.js";
8
+
9
+ /**
10
+ * Integration test: connect to the test weather server via stdio,
11
+ * discover skills, load a skill, verify tool schemas in result, call a gated tool.
12
+ */
13
+ describe("skill integration (weather server)", () => {
14
+ const manager = new McpClientManager();
15
+ const registry = new SkillRegistry();
16
+ const enabledTools = new Set<string>();
17
+ let client: Client;
18
+
19
+ beforeAll(async () => {
20
+ await manager.connectAll({
21
+ mcpServers: {
22
+ "test-weather": {
23
+ type: "stdio",
24
+ command: "node",
25
+ args: ["dist/test-servers/weather-stdio.js"],
26
+ },
27
+ },
28
+ });
29
+
30
+ const c = manager.getClient("test-weather");
31
+ if (!c) throw new Error("Expected client for test-weather");
32
+ client = c;
33
+ });
34
+
35
+ afterAll(async () => {
36
+ await manager.disconnectAll();
37
+ });
38
+
39
+ it("connects and discovers tools", () => {
40
+ expect(manager.getConnectedServers()).toEqual(["test-weather"]);
41
+ const tools = manager.getTools();
42
+ expect(tools.map((t) => t.name).sort()).toEqual([
43
+ "check_weather_for_city",
44
+ "check_weekly_forecast_for_city",
45
+ "echo",
46
+ ]);
47
+ });
48
+
49
+ it("discovers skill:// resources with frontmatter", async () => {
50
+ const skills = await discoverSkillsFromServer(client, "test-weather");
51
+
52
+ expect(skills).toHaveLength(1);
53
+ expect(skills[0].name).toBe("weather");
54
+ expect(skills[0].description).toBe("Check current weather and weekly forecasts for any city");
55
+ expect(skills[0].allowedTools).toEqual([
56
+ "check_weather_for_city",
57
+ "check_weekly_forecast_for_city",
58
+ ]);
59
+ expect(skills[0].uri).toBe("skill://weather/SKILL.md");
60
+ expect(skills[0].serverName).toBe("test-weather");
61
+
62
+ registry.registerAll(skills);
63
+ });
64
+
65
+ it("formats skills for system prompt", () => {
66
+ const prompt = formatMcpSkillsForPrompt(registry.getAll());
67
+ expect(prompt).toContain("<available_mcp_skills>");
68
+ expect(prompt).toContain("<name>weather</name>");
69
+ expect(prompt).toContain("load_skill");
70
+ });
71
+
72
+ it("load_skill returns skill body and reports activated tools", async () => {
73
+ const tool = createLoadSkillTool({
74
+ registry,
75
+ mcpManager: manager,
76
+ enabledTools,
77
+ });
78
+
79
+ const result = await tool.execute(
80
+ "call-1",
81
+ { name: "weather" },
82
+ undefined,
83
+ undefined,
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ {} as any,
86
+ );
87
+
88
+ expect(result.details.error).toBeUndefined();
89
+ expect(result.details.activatedTools).toEqual([
90
+ "check_weather_for_city",
91
+ "check_weekly_forecast_for_city",
92
+ ]);
93
+ const text = result.content[0];
94
+ expect(text.type).toBe("text");
95
+ // Skill body should mention the tools
96
+ expect("text" in text && text.text).toContain("check_weather_for_city");
97
+ // Tools should now be in enabledTools set
98
+ expect(enabledTools.has("check_weather_for_city")).toBe(true);
99
+ expect(enabledTools.has("check_weekly_forecast_for_city")).toBe(true);
100
+ });
101
+
102
+ it("calls gated tools via MCP client", async () => {
103
+ const weather = await client.callTool({
104
+ name: "check_weather_for_city",
105
+ arguments: { city: "Tokyo" },
106
+ });
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ expect((weather.content as any)[0].text).toContain("26");
109
+ expect(weather.structuredContent).toEqual({
110
+ temperature: 26,
111
+ conditions: "Sunny",
112
+ humidity: 55,
113
+ city: "Tokyo",
114
+ });
115
+
116
+ const forecast = await client.callTool({
117
+ name: "check_weekly_forecast_for_city",
118
+ arguments: { city: "London" },
119
+ });
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ expect((forecast.content as any)[0].text).toContain("London");
122
+ expect(forecast.structuredContent).toHaveProperty("city", "London");
123
+ expect(forecast.structuredContent).toHaveProperty("forecast");
124
+ });
125
+
126
+ it("load_skill returns error for unknown skill", async () => {
127
+ const tool = createLoadSkillTool({
128
+ registry,
129
+ mcpManager: manager,
130
+ enabledTools,
131
+ });
132
+
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ const result = await tool.execute("call-2", { name: "nope" }, undefined, undefined, {} as any);
135
+ expect(result.details.error).toBe("not_found");
136
+ const text = result.content[0];
137
+ expect("text" in text && text.text).toContain("not found");
138
+ });
139
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { SkillRegistry, type McpSkillMetadata } from "./skill-registry.js";
3
+
4
+ function makeSkill(overrides: Partial<McpSkillMetadata> = {}): McpSkillMetadata {
5
+ return {
6
+ name: "test-skill",
7
+ description: "A test skill",
8
+ uri: "skill://test/SKILL.md",
9
+ serverName: "test-server",
10
+ allowedTools: ["tool_a"],
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ describe("SkillRegistry", () => {
16
+ it("registers and retrieves a skill", () => {
17
+ const reg = new SkillRegistry();
18
+ const skill = makeSkill();
19
+ reg.register(skill);
20
+
21
+ expect(reg.get("test-skill")).toEqual(skill);
22
+ expect(reg.size).toBe(1);
23
+ });
24
+
25
+ it("getAll returns all registered skills", () => {
26
+ const reg = new SkillRegistry();
27
+ reg.register(makeSkill({ name: "a" }));
28
+ reg.register(makeSkill({ name: "b" }));
29
+
30
+ expect(reg.getAll()).toHaveLength(2);
31
+ });
32
+
33
+ it("overwrites on duplicate name", () => {
34
+ const reg = new SkillRegistry();
35
+ reg.register(makeSkill({ description: "first" }));
36
+ reg.register(makeSkill({ description: "second" }));
37
+
38
+ expect(reg.size).toBe(1);
39
+ expect(reg.get("test-skill")?.description).toBe("second");
40
+ });
41
+
42
+ it("unregister removes by name", () => {
43
+ const reg = new SkillRegistry();
44
+ reg.register(makeSkill());
45
+
46
+ expect(reg.unregister("test-skill")).toBe(true);
47
+ expect(reg.get("test-skill")).toBeUndefined();
48
+ expect(reg.unregister("test-skill")).toBe(false);
49
+ });
50
+
51
+ it("unregisterByServer removes all skills from a server", () => {
52
+ const reg = new SkillRegistry();
53
+ reg.register(makeSkill({ name: "a", serverName: "srv1" }));
54
+ reg.register(makeSkill({ name: "b", serverName: "srv1" }));
55
+ reg.register(makeSkill({ name: "c", serverName: "srv2" }));
56
+
57
+ reg.unregisterByServer("srv1");
58
+
59
+ expect(reg.size).toBe(1);
60
+ expect(reg.get("c")).toBeDefined();
61
+ });
62
+
63
+ it("registerAll registers multiple skills", () => {
64
+ const reg = new SkillRegistry();
65
+ reg.registerAll([makeSkill({ name: "x" }), makeSkill({ name: "y" })]);
66
+
67
+ expect(reg.size).toBe(2);
68
+ });
69
+
70
+ it("clear removes everything", () => {
71
+ const reg = new SkillRegistry();
72
+ reg.registerAll([makeSkill({ name: "a" }), makeSkill({ name: "b" })]);
73
+
74
+ reg.clear();
75
+ expect(reg.size).toBe(0);
76
+ expect(reg.getAll()).toEqual([]);
77
+ });
78
+
79
+ it("get returns undefined for unknown name", () => {
80
+ const reg = new SkillRegistry();
81
+ expect(reg.get("nonexistent")).toBeUndefined();
82
+ });
83
+ });
@@ -0,0 +1,69 @@
1
+ /** Metadata for a skill discovered from an MCP server. */
2
+ export interface McpSkillMetadata {
3
+ /** Skill name from SKILL.md frontmatter. */
4
+ name: string;
5
+ /** Short description from SKILL.md frontmatter. */
6
+ description: string;
7
+ /** The skill:// URI for the SKILL.md resource. */
8
+ uri: string;
9
+ /** Which MCP server this skill came from. */
10
+ serverName: string;
11
+ /** Tool names this skill gates via allowed-tools frontmatter. */
12
+ allowedTools: string[];
13
+ }
14
+
15
+ /**
16
+ * Registry for MCP-discovered skills.
17
+ *
18
+ * Holds skill metadata discovered from MCP servers. Skills are registered
19
+ * during MCP connection and looked up when the model invokes load_skill.
20
+ */
21
+ export class SkillRegistry {
22
+ private skills = new Map<string, McpSkillMetadata>();
23
+
24
+ /** Register a skill. Overwrites any existing skill with the same name. */
25
+ register(skill: McpSkillMetadata): void {
26
+ this.skills.set(skill.name, skill);
27
+ }
28
+
29
+ /** Register multiple skills at once. */
30
+ registerAll(skills: McpSkillMetadata[]): void {
31
+ for (const skill of skills) {
32
+ this.register(skill);
33
+ }
34
+ }
35
+
36
+ /** Get a skill by name. */
37
+ get(name: string): McpSkillMetadata | undefined {
38
+ return this.skills.get(name);
39
+ }
40
+
41
+ /** Get all registered skills. */
42
+ getAll(): McpSkillMetadata[] {
43
+ return [...this.skills.values()];
44
+ }
45
+
46
+ /** Remove a skill by name. */
47
+ unregister(name: string): boolean {
48
+ return this.skills.delete(name);
49
+ }
50
+
51
+ /** Remove all skills from a specific MCP server. */
52
+ unregisterByServer(serverName: string): void {
53
+ for (const [name, skill] of this.skills) {
54
+ if (skill.serverName === serverName) {
55
+ this.skills.delete(name);
56
+ }
57
+ }
58
+ }
59
+
60
+ /** Remove all skills. */
61
+ clear(): void {
62
+ this.skills.clear();
63
+ }
64
+
65
+ /** Number of registered skills. */
66
+ get size(): number {
67
+ return this.skills.size;
68
+ }
69
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Test MCP weather server — exposes a weather skill with gated tools.
3
+ *
4
+ * Registers:
5
+ * - skill://weather/SKILL.md — a skill resource with frontmatter
6
+ * - check_weather_for_city — current weather (gated by skill)
7
+ * - check_weekly_forecast_for_city — 7-day forecast (gated by skill)
8
+ * - echo — ungated baseline tool
9
+ */
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { z } from "zod";
12
+
13
+ const SKILL_CONTENT = `---
14
+ name: weather
15
+ description: Check current weather and weekly forecasts for any city
16
+ allowed-tools:
17
+ - check_weather_for_city
18
+ - check_weekly_forecast_for_city
19
+ ---
20
+
21
+ # Weather Forecasting Skill
22
+
23
+ Use the weather tools to look up conditions for any city.
24
+
25
+ - **check_weather_for_city** — returns current temperature, conditions, and humidity
26
+ - **check_weekly_forecast_for_city** — returns a 7-day forecast summary
27
+
28
+ Always confirm the city name with the user before calling.
29
+ `;
30
+
31
+ const WEATHER_DATA: Record<string, { temp: number; conditions: string; humidity: number }> = {
32
+ london: { temp: 14, conditions: "Cloudy", humidity: 78 },
33
+ tokyo: { temp: 26, conditions: "Sunny", humidity: 55 },
34
+ "new york": { temp: 22, conditions: "Partly cloudy", humidity: 62 },
35
+ sydney: { temp: 19, conditions: "Clear", humidity: 45 },
36
+ };
37
+
38
+ const FORECAST_DATA: Record<string, string> = {
39
+ london:
40
+ "Mon 13°C Rain | Tue 14°C Cloudy | Wed 15°C Cloudy | Thu 12°C Rain | Fri 14°C Sunny | Sat 16°C Sunny | Sun 15°C Cloudy",
41
+ tokyo:
42
+ "Mon 27°C Sunny | Tue 28°C Sunny | Wed 25°C Rain | Thu 24°C Cloudy | Fri 26°C Sunny | Sat 27°C Sunny | Sun 26°C Partly cloudy",
43
+ "new york":
44
+ "Mon 23°C Sunny | Tue 24°C Sunny | Wed 20°C Thunderstorms | Thu 18°C Cloudy | Fri 21°C Sunny | Sat 22°C Sunny | Sun 23°C Clear",
45
+ sydney:
46
+ "Mon 18°C Clear | Tue 20°C Sunny | Wed 19°C Cloudy | Thu 17°C Rain | Fri 18°C Cloudy | Sat 21°C Sunny | Sun 20°C Clear",
47
+ };
48
+
49
+ export function createWeatherServer(): McpServer {
50
+ const server = new McpServer(
51
+ { name: "test-weather-server", version: "0.1.0" },
52
+ { capabilities: { resources: {} } },
53
+ );
54
+
55
+ // Skill resource
56
+ server.registerResource(
57
+ "weather-skill",
58
+ "skill://weather/SKILL.md",
59
+ {
60
+ description: "Weather forecasting skill instructions",
61
+ mimeType: "text/markdown",
62
+ },
63
+ async () => ({
64
+ contents: [
65
+ {
66
+ uri: "skill://weather/SKILL.md",
67
+ text: SKILL_CONTENT,
68
+ mimeType: "text/markdown",
69
+ },
70
+ ],
71
+ }),
72
+ );
73
+
74
+ // Tool: current weather (gated by skill)
75
+ server.registerTool(
76
+ "check_weather_for_city",
77
+ {
78
+ description: "Get current weather conditions for a city",
79
+ inputSchema: { city: z.string().describe("City name, e.g. 'London'") },
80
+ outputSchema: {
81
+ temperature: z.number().describe("Temperature in Celsius"),
82
+ conditions: z.string().describe("Weather conditions description"),
83
+ humidity: z.number().describe("Humidity percentage"),
84
+ city: z.string().describe("City name"),
85
+ },
86
+ annotations: { readOnlyHint: true },
87
+ },
88
+ async ({ city }) => {
89
+ const data = WEATHER_DATA[city.toLowerCase()];
90
+ if (!data) {
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text" as const,
95
+ text: `No weather data available for "${city}". Try: London, Tokyo, New York, or Sydney.`,
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ const structuredContent = {
101
+ temperature: data.temp,
102
+ conditions: data.conditions,
103
+ humidity: data.humidity,
104
+ city,
105
+ };
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text" as const,
110
+ text: `Weather in ${city}: ${data.conditions}, ${data.temp}°C, humidity ${data.humidity}%`,
111
+ },
112
+ ],
113
+ structuredContent,
114
+ };
115
+ },
116
+ );
117
+
118
+ // Tool: weekly forecast (gated by skill)
119
+ server.registerTool(
120
+ "check_weekly_forecast_for_city",
121
+ {
122
+ description: "Get a 7-day weather forecast for a city",
123
+ inputSchema: { city: z.string().describe("City name, e.g. 'Tokyo'") },
124
+ outputSchema: {
125
+ city: z.string().describe("City name"),
126
+ forecast: z.string().describe("7-day forecast summary"),
127
+ },
128
+ annotations: { readOnlyHint: true },
129
+ },
130
+ async ({ city }) => {
131
+ const forecast = FORECAST_DATA[city.toLowerCase()];
132
+ if (!forecast) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text" as const,
137
+ text: `No forecast data available for "${city}". Try: London, Tokyo, New York, or Sydney.`,
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ const structuredContent = { city, forecast };
143
+ return {
144
+ content: [{ type: "text" as const, text: `Forecast for ${city}: ${forecast}` }],
145
+ structuredContent,
146
+ };
147
+ },
148
+ );
149
+
150
+ // Ungated tool (always available, not behind a skill)
151
+ server.registerTool(
152
+ "echo",
153
+ {
154
+ description: "Echoes back the input message",
155
+ inputSchema: { message: z.string() },
156
+ outputSchema: {
157
+ echo: z.string().describe("The echoed message"),
158
+ },
159
+ annotations: { readOnlyHint: true },
160
+ },
161
+ async ({ message }) => {
162
+ const structuredContent = { echo: message };
163
+ return {
164
+ content: [{ type: "text" as const, text: `Echo: ${message}` }],
165
+ structuredContent,
166
+ };
167
+ },
168
+ );
169
+
170
+ return server;
171
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * stdio entrypoint for the test weather server.
3
+ * Run with: node dist/test-servers/weather-stdio.js
4
+ */
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { createWeatherServer } from "./weather-server.js";
7
+
8
+ const server = createWeatherServer();
9
+ const transport = new StdioServerTransport();
10
+ await server.connect(transport);
11
+ console.error("Test weather server running on stdio");
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from "node:fs";
4
+ import { rpcCall } from "./rpc-client.js";
5
+
6
+ interface ServerInfo {
7
+ name: string;
8
+ toolCount: number;
9
+ examples: string[];
10
+ }
11
+
12
+ interface ToolSummary {
13
+ name: string;
14
+ description: string;
15
+ hasStructuredOutput: boolean;
16
+ }
17
+
18
+ interface ToolDetails {
19
+ name: string;
20
+ description: string;
21
+ inputSchema: Record<string, unknown>;
22
+ outputSchema?: Record<string, unknown>;
23
+ annotations?: Record<string, unknown>;
24
+ }
25
+
26
+ async function main(): Promise<void> {
27
+ const rawArgs = process.argv.slice(2);
28
+
29
+ // Extract --out <file> flag (can appear anywhere)
30
+ let outFile: string | undefined;
31
+ const args: string[] = [];
32
+ for (let i = 0; i < rawArgs.length; i++) {
33
+ if (rawArgs[i] === "--out" && i + 1 < rawArgs.length) {
34
+ outFile = rawArgs[++i];
35
+ } else {
36
+ args.push(rawArgs[i]);
37
+ }
38
+ }
39
+
40
+ try {
41
+ // No args or --help → list servers
42
+ if (args.length === 0 || (args.length === 1 && isHelp(args[0]))) {
43
+ await listServers();
44
+ return;
45
+ }
46
+
47
+ const server = args[0];
48
+
49
+ // <server> --help or just <server> → list tools
50
+ if (args.length === 1 || (args.length === 2 && isHelp(args[1]))) {
51
+ await listTools(server);
52
+ return;
53
+ }
54
+
55
+ const tool = args[1];
56
+
57
+ // <server> <tool> --help or just <server> <tool> → describe tool
58
+ if (args.length === 2 || (args.length === 3 && isHelp(args[2]))) {
59
+ await describeTool(server, tool);
60
+ return;
61
+ }
62
+
63
+ // <server> <tool> <json-args> → call tool
64
+ await callTool(server, tool, args[2], outFile);
65
+ } catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
68
+ console.error("Error: tool-cli server not running. Is mcpi-ext loaded?");
69
+ } else {
70
+ console.error(`Error: ${message}`);
71
+ }
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ async function listServers(): Promise<void> {
77
+ const result = (await rpcCall("listServers")) as { servers: ServerInfo[] };
78
+ const { servers } = result;
79
+
80
+ if (servers.length === 0) {
81
+ console.log("No MCP servers connected.");
82
+ return;
83
+ }
84
+
85
+ console.log("Available servers:");
86
+ for (const s of servers) {
87
+ const examples = s.examples.length > 0 ? ` (e.g. ${s.examples.join(", ")})` : "";
88
+ console.log(` ${s.name.padEnd(20)} ${s.toolCount} tool(s)${examples}`);
89
+ }
90
+ console.log("");
91
+ console.log("Use: tool-cli <server> to list tools");
92
+ }
93
+
94
+ async function listTools(server: string): Promise<void> {
95
+ const result = (await rpcCall("listTools", { server })) as {
96
+ server: string;
97
+ tools: ToolSummary[];
98
+ };
99
+
100
+ if (result.tools.length === 0) {
101
+ console.log(`${server} — no tools`);
102
+ return;
103
+ }
104
+
105
+ console.log(`${server} — ${result.tools.length} tool(s):`);
106
+ for (const t of result.tools) {
107
+ const badge = t.hasStructuredOutput ? " [json]" : "";
108
+ console.log(` ${t.name.padEnd(30)} ${t.description}${badge}`);
109
+ }
110
+ console.log("");
111
+ console.log("Use: tool-cli <server> <tool> for full schema");
112
+ }
113
+
114
+ async function describeTool(server: string, tool: string): Promise<void> {
115
+ const result = (await rpcCall("describeTool", { server, tool })) as ToolDetails;
116
+
117
+ console.log(`${result.name} — ${result.description}`);
118
+ console.log("");
119
+ console.log("Input schema:");
120
+ console.log(formatSchema(result.inputSchema));
121
+
122
+ if (result.outputSchema) {
123
+ console.log("");
124
+ console.log("Output schema (structured JSON output):");
125
+ console.log(formatSchema(result.outputSchema));
126
+ }
127
+
128
+ if (result.annotations && Object.keys(result.annotations).length > 0) {
129
+ console.log("");
130
+ console.log("Annotations:");
131
+ for (const [key, val] of Object.entries(result.annotations)) {
132
+ console.log(` ${key}: ${JSON.stringify(val)}`);
133
+ }
134
+ }
135
+
136
+ console.log("");
137
+ console.log(`Use: tool-cli ${server} ${tool} '{"key":"value"}' to call`);
138
+ }
139
+
140
+ async function callTool(
141
+ server: string,
142
+ tool: string,
143
+ argsJson: string,
144
+ outFile?: string,
145
+ ): Promise<void> {
146
+ let toolArgs: Record<string, unknown>;
147
+ try {
148
+ toolArgs = JSON.parse(argsJson) as Record<string, unknown>;
149
+ } catch {
150
+ console.error(`Error: invalid JSON arguments: ${argsJson}`);
151
+ process.exit(1);
152
+ }
153
+
154
+ const result = (await rpcCall("callTool", { server, tool, arguments: toolArgs })) as {
155
+ content: unknown[];
156
+ isError?: boolean;
157
+ structuredContent?: Record<string, unknown>;
158
+ };
159
+
160
+ if (result.isError) {
161
+ // Error content goes to stderr so stdout stays clean for piping
162
+ if (result.structuredContent) {
163
+ console.error(JSON.stringify(result.structuredContent, null, 2));
164
+ } else if (result.content) {
165
+ for (const item of result.content) {
166
+ const entry = item as Record<string, unknown>;
167
+ if (entry.type === "text") {
168
+ console.error(entry.text);
169
+ } else {
170
+ console.error(JSON.stringify(entry, null, 2));
171
+ }
172
+ }
173
+ }
174
+ process.exit(1);
175
+ return;
176
+ }
177
+
178
+ const output = formatOutput(result.structuredContent, result.content);
179
+ if (!output) return;
180
+
181
+ if (outFile) {
182
+ writeFileSync(outFile, output, "utf-8");
183
+ console.log(`Written to: ${outFile}`);
184
+ } else {
185
+ console.log(output);
186
+ }
187
+ }
188
+
189
+ /** Build the output string from structured or raw content. */
190
+ function formatOutput(structuredContent?: Record<string, unknown>, content?: unknown[]): string {
191
+ if (structuredContent) {
192
+ return JSON.stringify(structuredContent, null, 2);
193
+ }
194
+ if (content) {
195
+ const parts: string[] = [];
196
+ for (const item of content) {
197
+ const entry = item as Record<string, unknown>;
198
+ if (entry.type === "text") {
199
+ parts.push(entry.text as string);
200
+ } else {
201
+ parts.push(JSON.stringify(entry, null, 2));
202
+ }
203
+ }
204
+ return parts.join("\n");
205
+ }
206
+ return "";
207
+ }
208
+
209
+ /** Format a JSON Schema as a compact, readable summary. */
210
+ function formatSchema(schema: Record<string, unknown>): string {
211
+ const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
212
+ const required = (schema.required as string[]) ?? [];
213
+
214
+ if (!props || Object.keys(props).length === 0) {
215
+ return " (no parameters)";
216
+ }
217
+
218
+ const lines: string[] = [];
219
+ for (const [name, prop] of Object.entries(props)) {
220
+ const type = (prop.type as string) ?? "unknown";
221
+ const req = required.includes(name) ? ", required" : "";
222
+ const desc = prop.description ? `: ${prop.description as string}` : "";
223
+ lines.push(` ${name} (${type}${req})${desc}`);
224
+ }
225
+ return lines.join("\n");
226
+ }
227
+
228
+ function isHelp(arg: string): boolean {
229
+ return arg === "--help" || arg === "-h";
230
+ }
231
+
232
+ void main();