1import { assert, describe } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const rule = createOxlintRuleHarness("t3code/no-raw-mcp-registration");
6const testFile = createOxlintRuleHarness("t3code/no-raw-mcp-registration", {
7 filename: "server.test.ts",
8});
9
10describe("t3code/no-raw-mcp-registration", () => {
11 rule.valid(
12 "allows the rest of effect/ai and type-only McpServer imports",
13 `
14 import { McpSchema, Tool, Toolkit } from "effect/ai";
15 import type { McpServer } from "effect/ai";
16 import { type McpServer as Server } from "effect/ai";
17 export type Service = McpServer.McpServer | Server.McpServer;
18 export const used = [McpSchema, Tool, Toolkit];
19 `,
20 );
21
22 rule.invalid(
23 "reports importing McpServer from effect/ai",
24 `
25 import { McpServer } from "effect/ai";
26 export const server = McpServer.McpServer;
27 `,
28 (output) => {
29 assert.match(output, /Only McpHttpServer may use Effect's McpServer/);
30 },
31 );
32
33 rule.invalid(
34 "reports importing McpServer under another name",
35 `
36 import { McpServer as Server } from "effect/ai";
37 export const registration = Server.toolkit(SomeToolkit);
38 `,
39 );
40
41 rule.invalid(
42 "reports a namespace import of effect/ai",
43 `
44 import * as Ai from "effect/ai";
45 export const registration = Ai.McpServer.toolkit(SomeToolkit);
46 `,
47 );
48
49 rule.invalid(
50 "reports importing the McpServer module directly",
51 `
52 import { toolkit } from "effect/ai/McpServer";
53 export const registration = toolkit(SomeToolkit);
54 `,
55 );
56
57 rule.invalid(
58 "reports re-exporting McpServer",
59 `
60 export { McpServer as Server } from "effect/ai";
61 `,
62 );
63
64 rule.invalid(
65 "reports re-exporting all of effect/ai",
66 `
67 export * from "effect/ai";
68 `,
69 );
70
71 rule.invalid(
72 "reports a dynamic import of the McpServer module",
73 `
74 export const load = () => import("effect/ai/McpServer");
75 `,
76 );
77
78 rule.invalid(
79 "reports a dynamic import written as a template",
80 `
81 export const load = () => import(\`effect/ai/McpServer\`);
82 `,
83 );
84
85 rule.invalid(
86 "reports McpServer.toolkit",
87 `
88 export const registration = McpServer.toolkit(SomeToolkit);
89 `,
90 (output) => {
91 assert.match(output, /McpServer\.toolkit registers on \/mcp/);
92 },
93 );
94
95 rule.invalid(
96 "reports McpServer.resource and McpServer.prompt",
97 `
98 export const resource = McpServer.resource({ uri: "t3://x", name: "x", content: "x" });
99 export const prompt = McpServer.prompt({ name: "x", content: () => "x" });
100 `,
101 );
102
103 rule.invalid(
104 "reports addTool on the McpServer service",
105 `
106 import * as Effect from "effect/Effect";
107 export const register = Effect.gen(function* () {
108 const server = yield* Service;
109 yield* server.addTool({ tool, annotations, handle: () => Effect.die("unchecked") });
110 });
111 `,
112 (output) => {
113 assert.match(output, /\.addTool registers on \/mcp/);
114 },
115 );
116
117 rule.invalid(
118 "reports a registration method read without calling it",
119 `
120 export const add = server.addTool;
121 `,
122 );
123
124 rule.invalid(
125 "reports a registration function taken by destructuring",
126 `
127 const { toolkit } = McpServer;
128 export const registration = toolkit(SomeToolkit);
129 `,
130 );
131
132 rule.invalid(
133 "reports a registration method taken by destructuring",
134 `
135 import * as Effect from "effect/Effect";
136 export const register = Effect.gen(function* () {
137 const { addTool } = yield* Service;
138 yield* addTool({ tool, annotations, handle: () => Effect.die("unchecked") });
139 });
140 `,
141 );
142
143 testFile.valid(
144 "lets tests import McpServer to build a server",
145 `
146 import { McpServer } from "effect/ai";
147 export const layer = McpServer.McpServer.layer;
148 `,
149 );
150
151 testFile.invalid(
152 "reports a registration in a test",
153 `
154 import { McpServer } from "effect/ai";
155 export const registration = McpServer.toolkit(SomeToolkit);
156 `,
157 );
158
159 testFile.invalid(
160 "reports a test importing McpServer under another name",
161 `
162 import { McpServer as Server } from "effect/ai";
163 export const registration = Server.toolkit(SomeToolkit);
164 `,
165 (output) => {
166 assert.match(output, /Tests import McpServer only as/);
167 },
168 );
169
170 testFile.invalid(
171 "reports a test importing effect/ai as a namespace",
172 `
173 import * as Ai from "effect/ai";
174 export const registration = Ai.McpServer.toolkit(SomeToolkit);
175 `,
176 );
177
178 testFile.invalid(
179 "reports a test importing the McpServer module directly",
180 `
181 import { toolkit } from "effect/ai/McpServer";
182 export const registration = toolkit(SomeToolkit);
183 `,
184 );
185});