httpapi-mcp-oauth.test.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { NodeHttpServer } from "@effect/platform-node"
  2. import { Session } from "@/session/session"
  3. import { describe, expect } from "bun:test"
  4. import { Effect, Layer } from "effect"
  5. import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
  6. import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi"
  7. import { McpApi, McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
  8. import { Authorization } from "../../src/server/routes/instance/httpapi/middleware/authorization"
  9. import { InstanceContextMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
  10. import {
  11. WorkspaceRouteContext,
  12. WorkspaceRoutingMiddleware,
  13. } from "../../src/server/routes/instance/httpapi/middleware/workspace-routing"
  14. import { testEffect } from "../lib/effect"
  15. const TestHttpApi = HttpApi.make("opencode-instance").addHttpApi(McpApi)
  16. const fakeSession = Layer.mock(Session.Service)({})
  17. const testMcpHandlers = HttpApiBuilder.group(TestHttpApi, "mcp", (handlers) =>
  18. Effect.succeed(
  19. handlers
  20. .handle("status", () => Effect.die("unexpected MCP status"))
  21. .handle("add", () => Effect.die("unexpected MCP add"))
  22. .handle("authStart", () =>
  23. Effect.succeed({ authorizationUrl: "https://auth.example/start", oauthState: "state-123" }),
  24. )
  25. .handle("authCallback", () => Effect.die("unexpected MCP authCallback"))
  26. .handle("authAuthenticate", () => Effect.die("unexpected MCP authAuthenticate"))
  27. .handle("authRemove", () => Effect.die("unexpected MCP authRemove"))
  28. .handle("connect", () => Effect.die("unexpected MCP connect"))
  29. .handle("disconnect", () => Effect.die("unexpected MCP disconnect")),
  30. ),
  31. )
  32. const passthroughAuthorization = Layer.succeed(
  33. Authorization,
  34. Authorization.of((effect) => effect),
  35. )
  36. const passthroughInstanceContext = Layer.succeed(
  37. InstanceContextMiddleware,
  38. InstanceContextMiddleware.of((effect) => effect),
  39. )
  40. const testWorkspaceRouting = Layer.succeed(
  41. WorkspaceRoutingMiddleware,
  42. WorkspaceRoutingMiddleware.of((effect) =>
  43. effect.pipe(Effect.provideService(WorkspaceRouteContext, WorkspaceRouteContext.of({ directory: process.cwd() }))),
  44. ),
  45. )
  46. const it = testEffect(
  47. HttpRouter.serve(
  48. HttpApiBuilder.layer(TestHttpApi).pipe(
  49. Layer.provide(testMcpHandlers),
  50. Layer.provide([passthroughAuthorization, passthroughInstanceContext, testWorkspaceRouting, fakeSession]),
  51. ),
  52. { disableListenLog: true, disableLogger: true },
  53. ).pipe(Layer.provideMerge(NodeHttpServer.layerTest)),
  54. )
  55. describe("mcp HttpApi OAuth", () => {
  56. it.live("preserves oauth state when starting OAuth", () =>
  57. Effect.gen(function* () {
  58. const response = yield* HttpClientRequest.post(McpPaths.auth.replace(":name", "demo")).pipe(HttpClient.execute)
  59. expect(response.status).toBe(200)
  60. expect(yield* response.json).toEqual({
  61. authorizationUrl: "https://auth.example/start",
  62. oauthState: "state-123",
  63. })
  64. }),
  65. )
  66. })