kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(mcp): harden project update/auth timeout/install chmod

Tin b18dfa8e 037ece51

+58 -16
+10 -2
packages/mcp/src/auth/auth-service.test.ts
··· 50 50 }); 51 51 52 52 it("reuses the cached token when validation succeeds", async () => { 53 + const timeoutSpy = vi.spyOn(AbortSignal, "timeout"); 53 54 loadCredentialsMock.mockResolvedValue({ 54 55 version: 1, 55 56 baseUrl: "https://api.example.com", 56 57 clientId: "kaneo-mcp", 57 58 accessToken: "cached-token", 58 59 }); 59 - globalThis.fetch = vi.fn().mockResolvedValue( 60 + const fetchMock = vi.fn().mockResolvedValue( 60 61 new Response(JSON.stringify({ user: { id: "user-1" } }), { 61 62 status: 200, 62 63 }), 63 - ) as typeof fetch; 64 + ); 65 + globalThis.fetch = fetchMock as typeof fetch; 64 66 65 67 const service = new AuthService({ 66 68 baseUrl: "https://api.example.com", ··· 68 70 }); 69 71 70 72 await expect(service.getAccessToken()).resolves.toBe("cached-token"); 73 + expect(timeoutSpy).toHaveBeenCalledWith(10_000); 74 + expect(fetchMock).toHaveBeenCalledTimes(1); 75 + const requestInit = fetchMock.mock.calls[0]?.[1] as 76 + | { signal?: AbortSignal } 77 + | undefined; 78 + expect(requestInit?.signal).toBeInstanceOf(AbortSignal); 71 79 expect(clearCredentialsMock).not.toHaveBeenCalled(); 72 80 expect(requestDeviceCodeMock).not.toHaveBeenCalled(); 73 81 });
+2
packages/mcp/src/auth/auth-service.ts
··· 32 32 token: string, 33 33 ): Promise<TokenValidationResult> { 34 34 try { 35 + const signal = AbortSignal.timeout(10_000); 35 36 const res = await fetch(`${this.baseUrl}/api/auth/get-session`, { 36 37 headers: { Authorization: `Bearer ${token}` }, 38 + signal, 37 39 }); 38 40 if (res.status === 401) { 39 41 return "invalid";
+5 -1
packages/mcp/src/install/index.ts
··· 303 303 for (const w of mergedWrites) { 304 304 await mkdir(dirname(w.configPath), { recursive: true }); 305 305 await writeFile(w.configPath, w.merged, { encoding: "utf8", mode: 0o600 }); 306 - await chmod(w.configPath, 0o600); 306 + try { 307 + await chmod(w.configPath, 0o600); 308 + } catch { 309 + /* ignore chmod failures on some FS */ 310 + } 307 311 writtenPaths.push(w.configPath); 308 312 } 309 313
+37
packages/mcp/src/tools/register.test.ts
··· 123 123 expect(result?.isError).toBe(false); 124 124 }); 125 125 126 + it("fetches the current project and sends a full body for update_project", async () => { 127 + const { server, tools } = createServerMock(); 128 + const client = { 129 + json: vi 130 + .fn() 131 + .mockResolvedValueOnce({ 132 + name: "Roadmap", 133 + slug: "roadmap", 134 + }) 135 + .mockResolvedValueOnce({ id: "project-1", name: "Roadmap v2" }), 136 + }; 137 + 138 + registerTools(server as never, { client: client as never }); 139 + 140 + const result = await tools.get("update_project")?.handler({ 141 + id: "project-1", 142 + name: "Roadmap v2", 143 + }); 144 + 145 + expect(client.json).toHaveBeenNthCalledWith(1, "/api/project/project-1", { 146 + method: "GET", 147 + }); 148 + const putCall = client.json.mock.calls[1]; 149 + expect(putCall?.[0]).toBe("/api/project/project-1"); 150 + const putBody = JSON.parse( 151 + String((putCall?.[1] as { body?: string })?.body ?? "{}"), 152 + ); 153 + expect(putBody).toEqual({ 154 + name: "Roadmap v2", 155 + icon: "Layout", 156 + slug: "roadmap", 157 + description: "", 158 + isPublic: false, 159 + }); 160 + expect(result?.isError).toBe(false); 161 + }); 162 + 126 163 it("returns an MCP error result when the client request fails", async () => { 127 164 const { server, tools } = createServerMock(); 128 165 const client = {
+4 -13
packages/mcp/src/tools/register.ts
··· 155 155 ? patch.icon 156 156 : typeof existing.icon === "string" 157 157 ? existing.icon 158 - : undefined; 158 + : "Layout"; 159 159 const slug = 160 160 patch.slug ?? 161 161 (typeof existing.slug === "string" ? existing.slug : ""); ··· 167 167 ? patch.description 168 168 : typeof existing.description === "string" 169 169 ? existing.description 170 - : undefined; 170 + : ""; 171 171 const isPublic = 172 172 patch.isPublic !== undefined 173 173 ? patch.isPublic 174 174 : typeof existing.isPublic === "boolean" 175 175 ? existing.isPublic 176 - : undefined; 176 + : false; 177 177 178 - const body: Record<string, unknown> = { name, slug }; 179 - if (icon !== undefined) { 180 - body.icon = icon; 181 - } 182 - if (description !== undefined) { 183 - body.description = description; 184 - } 185 - if (isPublic !== undefined) { 186 - body.isPublic = isPublic; 187 - } 178 + const body = { name, icon, slug, description, isPublic }; 188 179 189 180 return client.json(`/api/project/${encodeURIComponent(id)}`, { 190 181 method: "PUT",