Skip to content

Commit 6bcfe64

Browse files
committed
Dedupe: one shared operation feeds API + MCP; set OpenAPI info
worker/operations.ts holds each op's schema + handler once; api.ts (REST + /openapi.json) and registry.ts (MCP) both project from it — sharing the Effect Schema directly (no duplicate declarations, no lossy OpenAPI round-trip since both are Effect). Set OpenAPI title/version/description.
1 parent 001d1be commit 6bcfe64

3 files changed

Lines changed: 59 additions & 44 deletions

File tree

‎worker/api.ts‎

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,31 @@
11
/**
2-
* The API — the single source of truth.
3-
*
4-
* Endpoints are defined once as an Effect `HttpApi`; the typed server, the
5-
* OpenAPI document (`/openapi.json`), and downstream artifacts (MCP, CLI) all
6-
* derive from this. Runs as a pure web fetch handler on Cloudflare Workers.
2+
* The REST API — defined once as an Effect HttpApi. The typed server and the
3+
* OpenAPI document (/openapi.json) both derive from this. Runs as a pure web
4+
* fetch handler on Cloudflare Workers.
75
*/
8-
import { Effect, FileSystem, Layer, Path, Schema } from "effect";
6+
import { FileSystem, Layer, Path } from "effect";
97
import { Etag, HttpPlatform } from "effect/unstable/http";
108
import * as HttpRouter from "effect/unstable/http/HttpRouter";
119
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
12-
import { detect } from "../src/lib/detect.ts";
13-
14-
// Response shape (top-level typed; per-format detail kept loose for now).
15-
const DetectionResult = Schema.Struct({
16-
domain: Schema.String,
17-
found: Schema.Array(Schema.String),
18-
apiCatalog: Schema.optional(Schema.Unknown),
19-
apiSchema: Schema.optional(Schema.Unknown),
20-
mcp: Schema.Array(Schema.Unknown),
21-
agentCard: Schema.optional(Schema.Unknown),
22-
agentSkills: Schema.optional(Schema.Unknown),
23-
llmsTxt: Schema.Boolean,
24-
errors: Schema.Array(Schema.String),
25-
});
10+
import * as OpenApi from "effect/unstable/httpapi/OpenApi";
11+
import { DetectionResult, DetectParams, runDetect } from "./operations.ts";
2612

2713
const Detect = HttpApiEndpoint.get("detect", "/api/:domain/detect", {
28-
params: Schema.Struct({ domain: Schema.String }),
14+
params: DetectParams,
2915
success: DetectionResult,
3016
});
3117

32-
export const Api = HttpApi.make("integrations.sh").add(
33-
HttpApiGroup.make("detect").add(Detect),
34-
);
18+
export const Api = HttpApi.make("integrations.sh")
19+
.add(HttpApiGroup.make("detect").add(Detect))
20+
.annotate(OpenApi.Title, "integrations.sh")
21+
.annotate(OpenApi.Version, "0.1.0")
22+
.annotate(
23+
OpenApi.Description,
24+
"Discover how to integrate with any service — APIs, MCP servers, GraphQL, CLIs — and detect what a domain exposes to agents.",
25+
);
3526

3627
const DetectGroup = HttpApiBuilder.group(Api, "detect", (handlers) =>
37-
handlers.handle("detect", (req: { readonly params: { readonly domain: string } }) =>
38-
Effect.promise(() => detect(req.params.domain.trim().toLowerCase()) as Promise<typeof DetectionResult.Type>),
39-
),
28+
handlers.handle("detect", (req: { readonly params: { readonly domain: string } }) => runDetect(req.params.domain)),
4029
);
4130

4231
const Platform = Layer.mergeAll(Path.layer, Etag.layerWeak, HttpPlatform.layer).pipe(

‎worker/operations.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Operations — the single source of truth, written once.
3+
*
4+
* Each operation's input schema, output schema, description, and handler live
5+
* here exactly once. The REST API (worker/api.ts → also emits /openapi.json) and
6+
* the MCP server (worker/registry.ts) are both projected from these — sharing
7+
* the Effect Schema directly rather than round-tripping through OpenAPI (lossless,
8+
* fully typed, since both transports are Effect).
9+
*/
10+
import { Effect, Schema } from "effect";
11+
import { detect } from "../src/lib/detect.ts";
12+
13+
export const DetectParams = Schema.Struct({ domain: Schema.String });
14+
15+
export const DetectionResult = Schema.Struct({
16+
domain: Schema.String,
17+
found: Schema.Array(Schema.String),
18+
apiCatalog: Schema.optional(Schema.Unknown),
19+
apiSchema: Schema.optional(Schema.Unknown),
20+
mcp: Schema.Array(Schema.Unknown),
21+
agentCard: Schema.optional(Schema.Unknown),
22+
agentSkills: Schema.optional(Schema.Unknown),
23+
llmsTxt: Schema.Boolean,
24+
errors: Schema.Array(Schema.String),
25+
});
26+
27+
export const DETECT_DESCRIPTION =
28+
"Detect a domain's agent-readiness: well-known manifests (api-catalog, MCP " +
29+
"server-card, agent-card, agent-skills, llms.txt) plus live capability " +
30+
"detection — MCP self-onboarding (DCR/CIMD) and the live OpenAPI schema.";
31+
32+
/** The detect handler, shared by REST and MCP. */
33+
export const runDetect = (domain: string): Effect.Effect<typeof DetectionResult.Type> =>
34+
Effect.promise(() => detect(domain.trim().toLowerCase()) as Promise<typeof DetectionResult.Type>);

‎worker/registry.ts‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,27 @@
11
/**
2-
* The tool registry — write once, projected to MCP (here) and REST (the worker's
3-
* /api routes). Each tool declares its schema + handler once; the same handler
4-
* backs every transport. The `integrations` CLI is the local client.
5-
*
6-
* Runs as a pure web fetch handler (Effect v4), so it lives in the Cloudflare
7-
* Worker alongside the static site.
2+
* MCP projection — the same operations (worker/operations.ts), exposed as MCP
3+
* tools. Schema + handler are shared with the REST API; only the transport
4+
* differs. Served over HTTP at /mcp (via a Durable Object for session state).
85
*/
9-
import { Effect, Layer, Schema } from "effect";
6+
import { Layer } from "effect";
107
import * as McpServer from "effect/unstable/ai/McpServer";
118
import * as Tool from "effect/unstable/ai/Tool";
129
import * as Toolkit from "effect/unstable/ai/Toolkit";
1310
import * as HttpRouter from "effect/unstable/http/HttpRouter";
14-
import { detect } from "../src/lib/detect.ts";
11+
import { DETECT_DESCRIPTION, DetectionResult, DetectParams, runDetect } from "./operations.ts";
1512

16-
// ── tools ─────────────────────────────────────────────────────────────────────
1713
const Detect = Tool.make("detect", {
18-
description:
19-
"Detect a domain's agent-readiness: well-known manifests (api-catalog, MCP " +
20-
"server-card, agent-card, agent-skills, llms.txt) plus live capability " +
21-
"detection — MCP self-onboarding (DCR/CIMD) and the live OpenAPI schema.",
22-
parameters: Schema.Struct({ domain: Schema.String }),
23-
success: Schema.Unknown,
14+
description: DETECT_DESCRIPTION,
15+
parameters: DetectParams,
16+
success: DetectionResult,
2417
});
2518

2619
export const toolkit = Toolkit.make(Detect);
2720

2821
const HandlersLayer = toolkit.toLayer({
29-
detect: ({ domain }) => Effect.promise(() => detect(String(domain).trim().toLowerCase())),
22+
detect: ({ domain }) => runDetect(domain),
3023
} as never);
3124

32-
// ── MCP over HTTP at /mcp ─────────────────────────────────────────────────────
3325
const McpLayer = Layer.mergeAll(
3426
McpServer.layerHttp({ name: "integrations.sh", version: "0.1.0", path: "/mcp" }),
3527
McpServer.toolkit(toolkit),

0 commit comments

Comments
 (0)