diff --git a/apps/gateway/package.json b/apps/gateway/package.json index 5384ff68..13720cbd 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -34,9 +34,9 @@ "@agentic/platform-types": "workspace:*", "@agentic/platform-validators": "workspace:*", "@hono/zod-validator": "catalog:", + "@modelcontextprotocol/sdk": "catalog:", "eventid": "catalog:", "hono": "catalog:", - "@modelcontextprotocol/sdk": "catalog:", "type-fest": "catalog:" }, "devDependencies": { diff --git a/apps/gateway/src/lib/enforce-rate-limit.ts b/apps/gateway/src/lib/enforce-rate-limit.ts index 8b2093e6..eae835d8 100644 --- a/apps/gateway/src/lib/enforce-rate-limit.ts +++ b/apps/gateway/src/lib/enforce-rate-limit.ts @@ -11,13 +11,15 @@ export async function enforceRateLimit( method, pathname }: { - id: string + id?: string interval: number maxPerInterval: number method: string pathname: string } ) { + assert(id, 400, 'Unauthenticated requests must have a valid IP address') + // TODO assert(ctx, 500, 'not implemented') assert(id, 500, 'not implemented') diff --git a/apps/gateway/src/lib/get-consumer.ts b/apps/gateway/src/lib/get-admin-consumer.ts similarity index 89% rename from apps/gateway/src/lib/get-consumer.ts rename to apps/gateway/src/lib/get-admin-consumer.ts index 1f611a60..d3969085 100644 --- a/apps/gateway/src/lib/get-consumer.ts +++ b/apps/gateway/src/lib/get-admin-consumer.ts @@ -2,7 +2,7 @@ import { assert } from '@agentic/platform-core' import type { AdminConsumer, Context } from './types' -export async function getConsumer( +export async function getAdminConsumer( ctx: Context, token: string ): Promise { diff --git a/apps/gateway/src/lib/get-deployment.ts b/apps/gateway/src/lib/get-admin-deployment.ts similarity index 94% rename from apps/gateway/src/lib/get-deployment.ts rename to apps/gateway/src/lib/get-admin-deployment.ts index b4edf3c3..702d9c4d 100644 --- a/apps/gateway/src/lib/get-deployment.ts +++ b/apps/gateway/src/lib/get-admin-deployment.ts @@ -4,7 +4,7 @@ import { parseFaasIdentifier } from '@agentic/platform-validators' import type { Context } from './types' -export async function getDeployment( +export async function getAdminDeployment( ctx: Context, identifier: string ): Promise<{ deployment: AdminDeployment; toolPath: string }> { diff --git a/apps/gateway/src/lib/resolve-origin-request.ts b/apps/gateway/src/lib/resolve-origin-request.ts index 39622988..54871e7b 100644 --- a/apps/gateway/src/lib/resolve-origin-request.ts +++ b/apps/gateway/src/lib/resolve-origin-request.ts @@ -2,8 +2,9 @@ import type { PricingPlan, RateLimit } from '@agentic/platform-types' import { assert } from '@agentic/platform-core' import type { AdminConsumer, Context, ResolvedOriginRequest } from './types' -import { getConsumer } from './get-consumer' -import { getDeployment } from './get-deployment' +import { enforceRateLimit } from './enforce-rate-limit' +import { getAdminConsumer } from './get-admin-consumer' +import { getAdminDeployment } from './get-admin-deployment' import { getTool } from './get-tool' import { updateOriginRequest } from './update-origin-request' @@ -23,8 +24,13 @@ export async function resolveOriginRequest( const { search, pathname } = requestUrl const method = req.method.toLowerCase() + const requestPathParts = pathname.split('/') + const requestPath = + requestPathParts[0] === 'mcp' + ? requestPathParts.slice(1).join('/') + : pathname - const { deployment, toolPath } = await getDeployment(ctx, pathname) + const { deployment, toolPath } = await getAdminDeployment(ctx, requestPath) const tool = getTool({ method, @@ -50,7 +56,7 @@ export async function resolveOriginRequest( .trim() if (token) { - consumer = await getConsumer(ctx, token) + consumer = await getAdminConsumer(ctx, token) assert(consumer, 401, `Invalid auth token "${token}"`) assert( consumer.isStripeSubscriptionActive, @@ -155,12 +161,11 @@ export async function resolveOriginRequest( } } - // enforce requests rate limits if (rateLimit) { await enforceRateLimit(ctx, { - id: consumer ? consumer.id : ip, - duration: rateLimit.interval * 1000, - max: rateLimit.maxPerInterval, + id: consumer?.id ?? ip, + interval: rateLimit.interval * 1000, + maxPerInterval: rateLimit.maxPerInterval, method, pathname }) diff --git a/apps/gateway/src/mcp.ts b/apps/gateway/src/mcp.ts new file mode 100644 index 00000000..32be3461 --- /dev/null +++ b/apps/gateway/src/mcp.ts @@ -0,0 +1,27 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' + +import type { Context, ResolvedOriginRequest } from './lib/types' + +// TODO: https://github.com/modelcontextprotocol/servers/blob/8fb7bbdab73eddb42aba72e8eab81102efe1d544/src/everything/sse.ts +// TODO: https://github.com/cloudflare/agents + +const transports: Map = new Map< + string, + SSEServerTransport +>() + +export async function handleMCPRequest( + ctx: Context, + resolvedOriginRequest: ResolvedOriginRequest +) { + const serverTransport = new SSEServerTransport() + const server = new McpServer({ + name: 'weather', + version: '1.0.0', + capabilities: { + resources: {}, + tools: {} + } + }) +} diff --git a/apps/gateway/src/worker.ts b/apps/gateway/src/worker.ts index d3459f56..1eb8e96c 100644 --- a/apps/gateway/src/worker.ts +++ b/apps/gateway/src/worker.ts @@ -62,6 +62,17 @@ export default { const resolvedOriginRequest = await resolveOriginRequest(ctx) try { + switch (resolvedOriginRequest.deployment.originAdapter.type) { + case 'openapi': + break + + case 'raw': + break + + case 'mcp': + break + } + const originReqCacheKey = await getOriginRequestCacheKey(originReq) originStartTime = Date.now() diff --git a/readme.md b/readme.md index 56401fc5..d03ee3fe 100644 --- a/readme.md +++ b/readme.md @@ -7,8 +7,6 @@ ## TODO -- **api gateway** - - signed requests - **webapp** - end-to-end working examples - raw @@ -25,7 +23,7 @@ - (openauth password emails and `sendCode`) - stripe-related billing emails - auth - - custom auth pages + - custom auth pages for `openauth` - re-add support for teams / organizations - consider switching to [consola](https://github.com/unjs/consola) for logging? - consider switching to `bun` (for `--hot` reloading!!) @@ -36,11 +34,23 @@ - validate stability of pricing plan slugs across deployments - same for pricing plan line-items - replace `ms` package -- API gateway MCP server vs OpenAPI API gateway -- share hono middleware and utils across apps/api and apps/gateway - - or combine these together? ehhhh - add username / team name blacklist - - admin, internal, mcp, etc + - admin, internal, mcp, sse, etc +- **API gateway** + - share hono middleware and utils across apps/api and apps/gateway + - or combine these together? ehhhh + - MCP server vs REST gateway on public and internal sides + - RAW: `METHOD gateway.agentic.so/deploymentIdentifier/` + - => Raw HTTP: `METHOD originUrl/` simple HTTP proxy request + - REST: `POST gateway.agentic.so/deploymentIdentifier/toolName` + - => MCP: `MCPClient.callTool` with JSON body parameters + - => OpenAPI: `GET/POST/ETC originUrl/toolName` operation with transformed JSON body params + - MCP: `mcp.agentic.so/deploymentIdentifier/sse` MCP server + - => MCP: `MCPClient.callTool` just proxying tool call + - => OpenAPI: `GET/POST/ETC originUrl/toolName` operation with transformed tool params + - add support for caching + - add support for custom headers on response + - signed requests ## License