@12-apps/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/package.json +45 -0
- package/src/auth/authorization-server-metadata.test.ts +71 -0
- package/src/auth/authorization-server-metadata.ts +73 -0
- package/src/auth/resource-metadata.test.ts +46 -0
- package/src/auth/resource-metadata.ts +68 -0
- package/src/dispatch/proxy.test.ts +130 -0
- package/src/dispatch/proxy.ts +126 -0
- package/src/index.ts +48 -0
- package/src/openapi/generate.test.ts +107 -0
- package/src/openapi/generate.ts +227 -0
- package/src/openapi/refs.test.ts +80 -0
- package/src/openapi/refs.ts +84 -0
- package/src/server/manifest.test.ts +51 -0
- package/src/server/manifest.ts +47 -0
- package/src/server/registry.test.ts +79 -0
- package/src/server/registry.ts +87 -0
- package/src/types.ts +115 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { dispatchTool } from "../dispatch/proxy";
|
|
2
|
+
import type { GeneratedTool, JsonSchema, RequestAuth } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The registry is the transport-agnostic seam between the generated tools and the
|
|
6
|
+
* MCP SDK. The consuming app owns the HTTP/JSON-RPC transport (mounting it at
|
|
7
|
+
* `/api/mcp`) and, per request, resolves {@link RequestAuth} and calls
|
|
8
|
+
* {@link ToolRegistry.listTools} / {@link ToolRegistry.callTool}. Keeping the
|
|
9
|
+
* SDK out of this package means the core stays testable and portable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** An MCP tool descriptor as advertised to clients (subset of the MCP schema). */
|
|
13
|
+
export interface McpToolDescriptor {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputSchema: JsonSchema;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** An MCP tool-call result (subset of the MCP schema). */
|
|
20
|
+
export interface McpToolResult {
|
|
21
|
+
content: Array<{ type: "text"; text: string }>;
|
|
22
|
+
isError: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ToolRegistry {
|
|
26
|
+
listTools(auth?: RequestAuth): McpToolDescriptor[];
|
|
27
|
+
callTool(
|
|
28
|
+
name: string,
|
|
29
|
+
args: Record<string, unknown>,
|
|
30
|
+
auth: RequestAuth,
|
|
31
|
+
): Promise<McpToolResult>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RegistryOptions {
|
|
35
|
+
tools: GeneratedTool[];
|
|
36
|
+
/** Origin the tools proxy to (usually the app's own public URL). */
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
/**
|
|
40
|
+
* Optional visibility filter — e.g. hide mutating tools, or tools whose
|
|
41
|
+
* required scope the caller lacks. Authorization is still enforced upstream;
|
|
42
|
+
* this only shapes what the agent is shown.
|
|
43
|
+
*/
|
|
44
|
+
isVisible?: (tool: GeneratedTool, auth?: RequestAuth) => boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function textResult(value: unknown, isError: boolean): McpToolResult {
|
|
48
|
+
const text =
|
|
49
|
+
typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
50
|
+
return { content: [{ type: "text", text }], isError };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createToolRegistry(options: RegistryOptions): ToolRegistry {
|
|
54
|
+
const byName = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
listTools(auth) {
|
|
58
|
+
return options.tools
|
|
59
|
+
.filter((tool) => (options.isVisible ? options.isVisible(tool, auth) : true))
|
|
60
|
+
.map((tool) => ({
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description,
|
|
63
|
+
inputSchema: tool.inputSchema,
|
|
64
|
+
}));
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async callTool(name, args, auth) {
|
|
68
|
+
const tool = byName.get(name);
|
|
69
|
+
if (!tool) return textResult(`Unknown tool: ${name}`, true);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await dispatchTool(tool, args, {
|
|
73
|
+
baseUrl: options.baseUrl,
|
|
74
|
+
bearer: auth.bearer,
|
|
75
|
+
fetchImpl: options.fetchImpl,
|
|
76
|
+
});
|
|
77
|
+
// A non-2xx from the endpoint (e.g. 403 tenant-forbidden) is surfaced to
|
|
78
|
+
// the agent as an error result, NOT thrown — the permission decision was
|
|
79
|
+
// made upstream and its message is the useful signal.
|
|
80
|
+
return textResult(result.body, !result.ok);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return textResult(`Tool dispatch failed: ${message}`, true);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the app-agnostic MCP layer.
|
|
3
|
+
*
|
|
4
|
+
* The design in one sentence: generate one MCP tool per OpenAPI operation, and
|
|
5
|
+
* dispatch each tool call by proxying to the real HTTP endpoint carrying the
|
|
6
|
+
* caller's bearer token — so an agent gets exactly the user's permissions and
|
|
7
|
+
* authorization stays entirely in the endpoints (never re-implemented here).
|
|
8
|
+
*
|
|
9
|
+
* This package is deliberately free of any app/domain specifics and of the MCP
|
|
10
|
+
* transport SDK: it turns a spec into tools and a tool call into an HTTP request.
|
|
11
|
+
* The consuming app supplies the OpenAPI document, the base URL, and an
|
|
12
|
+
* {@link AuthResolver}; it binds {@link ToolRegistry} to the MCP transport.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A JSON Schema object (draft 2020-12). We treat schemas opaquely and forward them. */
|
|
16
|
+
export type JsonSchema = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/** Where an operation parameter is carried in the HTTP request. */
|
|
19
|
+
export type ParameterLocation = "path" | "query" | "header";
|
|
20
|
+
|
|
21
|
+
/** One OpenAPI operation parameter, retained so the dispatcher can route args. */
|
|
22
|
+
export interface ToolParameter {
|
|
23
|
+
name: string;
|
|
24
|
+
in: ParameterLocation;
|
|
25
|
+
required: boolean;
|
|
26
|
+
schema: JsonSchema;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A single generated MCP tool: an agent-facing input schema plus everything the
|
|
31
|
+
* dispatcher needs to reconstruct the HTTP call. This is the unit that is both
|
|
32
|
+
* served to agents and committed to the drift manifest.
|
|
33
|
+
*/
|
|
34
|
+
export interface GeneratedTool {
|
|
35
|
+
/** Stable tool id: the operationId, or a `method_path` slug when absent. */
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
/** Uppercase HTTP method, e.g. "GET", "POST". */
|
|
39
|
+
method: string;
|
|
40
|
+
/** OpenAPI path template, e.g. "/products/{id}". */
|
|
41
|
+
path: string;
|
|
42
|
+
/** Agent-facing input schema (params + flattened request-body properties). */
|
|
43
|
+
inputSchema: JsonSchema;
|
|
44
|
+
/** Documented success-response schema, when the spec provides one. */
|
|
45
|
+
outputSchema?: JsonSchema;
|
|
46
|
+
/** Path/query/header parameters, in declaration order. */
|
|
47
|
+
parameters: ToolParameter[];
|
|
48
|
+
/** Top-level property names sourced from the request body (routed to the body). */
|
|
49
|
+
bodyProps: string[];
|
|
50
|
+
/** True when the request body is not an object (sent verbatim as the payload). */
|
|
51
|
+
bodyIsWhole: boolean;
|
|
52
|
+
/** True for POST/PUT/PATCH/DELETE — a write. Consumers may gate these. */
|
|
53
|
+
mutating: boolean;
|
|
54
|
+
/** OpenAPI security requirement names referenced by the operation, if any. */
|
|
55
|
+
security: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The committed source-of-truth artifact the drift gate (`mcp:check`) diffs. */
|
|
59
|
+
export interface ToolManifest {
|
|
60
|
+
/** Bumped on any intentional shape change (mirrors the golden-catalog convention). */
|
|
61
|
+
version: number;
|
|
62
|
+
/** Human label for the spec the tools were generated from. */
|
|
63
|
+
source: string;
|
|
64
|
+
tools: GeneratedTool[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Options controlling tool generation from an OpenAPI document. */
|
|
68
|
+
export interface GenerateOptions {
|
|
69
|
+
/** Only include operations whose method is in this set (default: all). */
|
|
70
|
+
includeMethods?: readonly string[];
|
|
71
|
+
/** Drop operations tagged with any of these (e.g. "internal"). */
|
|
72
|
+
excludeTags?: readonly string[];
|
|
73
|
+
/** Predicate to include/exclude an operation by (method, path). */
|
|
74
|
+
filter?: (method: string, path: string) => boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The caller's resolved authorization for a single request. The dispatcher only
|
|
79
|
+
* ever forwards `bearer` upstream; identity/permission checks happen at the
|
|
80
|
+
* endpoint. `subject`/`scopes` are surfaced for logging and tool-visibility
|
|
81
|
+
* decisions, not for authorization.
|
|
82
|
+
*/
|
|
83
|
+
export interface RequestAuth {
|
|
84
|
+
/** The bearer token to forward to the upstream endpoint (verbatim). */
|
|
85
|
+
bearer: string;
|
|
86
|
+
/** Token subject (for logging only). */
|
|
87
|
+
subject?: string;
|
|
88
|
+
/** Granted scopes (for optional tool visibility filtering). */
|
|
89
|
+
scopes?: readonly string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* App-supplied hook that turns an incoming request into {@link RequestAuth}, or
|
|
94
|
+
* `null` when unauthenticated. For the OAuth resource-server mode this validates
|
|
95
|
+
* the access token (signature/audience/scope) and returns the token to forward.
|
|
96
|
+
*/
|
|
97
|
+
export type AuthResolver = (request: Request) => Promise<RequestAuth | null>;
|
|
98
|
+
|
|
99
|
+
/** Configuration for a proxying dispatch. */
|
|
100
|
+
export interface DispatchConfig {
|
|
101
|
+
/** Origin the tools are proxied to, e.g. "https://app.example.com". */
|
|
102
|
+
baseUrl: string;
|
|
103
|
+
/** The caller's bearer, forwarded as `Authorization: Bearer <token>`. */
|
|
104
|
+
bearer: string;
|
|
105
|
+
/** Injectable fetch (defaults to global fetch) — eases testing. */
|
|
106
|
+
fetchImpl?: typeof fetch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The upstream response, normalized for the MCP layer. */
|
|
110
|
+
export interface DispatchResult {
|
|
111
|
+
status: number;
|
|
112
|
+
ok: boolean;
|
|
113
|
+
/** Parsed JSON body when the response was JSON, else the raw text. */
|
|
114
|
+
body: unknown;
|
|
115
|
+
}
|