@farm.js/mcp 0.0.0-stage → 0.1.0-beta.1
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/LICENSE +22 -0
- package/README.md +96 -2
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +630 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +6 -0
- package/package.json +53 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Farm.js Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,97 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @farm.js/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Experimental Streamable HTTP MCP servers with typed Farm API routes and standalone tools.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineConfig } from "@farm.js/core/config";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
mcp: {
|
|
10
|
+
authorize: async ({ request }) =>
|
|
11
|
+
request.headers.get("authorization") ? { subject: "api-user" } : false,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Opt an endpoint in where it is defined:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createEndpoint } from "@farm.js/core/api";
|
|
20
|
+
|
|
21
|
+
export const GET = createEndpoint(
|
|
22
|
+
{
|
|
23
|
+
method: "GET",
|
|
24
|
+
mcp: { name: "list_projects", description: "List visible projects." },
|
|
25
|
+
},
|
|
26
|
+
listProjects,
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`mcp: true` derives a name from the route method and path. The endpoint defaults to `/api/mcp`.
|
|
31
|
+
Selected routes keep their normal Farm validation,
|
|
32
|
+
endpoint middleware, request context, error responses, and cancellation behavior.
|
|
33
|
+
|
|
34
|
+
Alternatively, compose server-only endpoint references and standalone tools in config:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { defineTool } from "@farm.js/mcp";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import { GET as listProjects } from "./src/app/api/projects/route";
|
|
40
|
+
import { getSession } from "./src/lib/session";
|
|
41
|
+
import { searchProjects } from "./src/lib/projects";
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
mcp: {
|
|
45
|
+
tools: [
|
|
46
|
+
{ endpoint: listProjects, name: "list_projects" },
|
|
47
|
+
defineTool({
|
|
48
|
+
name: "search_projects",
|
|
49
|
+
description: "Search projects visible to the current user.",
|
|
50
|
+
inputSchema: z.object({ query: z.string().trim().min(1) }),
|
|
51
|
+
readOnlyHint: true,
|
|
52
|
+
destructiveHint: false,
|
|
53
|
+
execute: ({ query }, { authorization, signal }) =>
|
|
54
|
+
searchProjects({ query, userId: authorization.subject, signal }),
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
authorize: async ({ request, tools, server }) => {
|
|
58
|
+
const session = await getSession(request);
|
|
59
|
+
if (!session) return false;
|
|
60
|
+
return {
|
|
61
|
+
subject: session.user.id,
|
|
62
|
+
tools: tools
|
|
63
|
+
.filter((tool) => session.scopes.includes(`${server.name}:${tool.name}`))
|
|
64
|
+
.map((tool) => tool.name),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Config references need an explicit path, for example
|
|
72
|
+
`createEndpoint("/api/projects", { method: "GET" }, handler)`, matching a mounted app route.
|
|
73
|
+
`mcp.tools` replaces automatic discovery; omit it to keep route-owned opt-ins. Bare references
|
|
74
|
+
such as `tools: [listProjects]` preserve route metadata. `authorize` receives a frozen catalog
|
|
75
|
+
with each tool's name and kind (`endpoint` or `standalone`), plus method/path for endpoints and
|
|
76
|
+
the resolved server identity. Its optional returned
|
|
77
|
+
`tools` list controls both discovery and invocation: omitted means all configured tools; `[]`
|
|
78
|
+
means none. Scopes and annotations alone never grant or restrict access. Keep endpoint middleware
|
|
79
|
+
for direct HTTP and argument-level permissions; standalone tools use `authorization` in `execute`
|
|
80
|
+
for argument-level checks.
|
|
81
|
+
|
|
82
|
+
Standalone tools need no separate HTTP route and can be the only tools in a server. Their
|
|
83
|
+
arguments match `inputSchema` directly (for example `{ query: "farm" }`, without a `query` or
|
|
84
|
+
`body` wrapper). The schema must describe an object and support input JSON Schema conversion:
|
|
85
|
+
Zod 4 works directly, as does Standard Schema with `~standard.jsonSchema.input()`. Farm runs
|
|
86
|
+
the original validator before execution, including async refinements and transforms; `execute`
|
|
87
|
+
receives the inferred, validated output and `{ authorization, request, signal }`.
|
|
88
|
+
|
|
89
|
+
Return JSON-serializable data, not a Web `Response`, stream, or MCP protocol envelope. Farm wraps
|
|
90
|
+
the result as text content and `structuredContent: { result: data }`. Pass `signal` to cancellable
|
|
91
|
+
work; cancellation cannot undo completed side effects. Denied tools never reach execution.
|
|
92
|
+
|
|
93
|
+
`apiMcp()` remains exported for advanced manual plugin composition with the same options.
|
|
94
|
+
Normal applications should use the top-level `mcp` config.
|
|
95
|
+
|
|
96
|
+
See the [API MCP guide](https://farmjs.dev/docs/plugins/mcp) and the runnable
|
|
97
|
+
[`examples/api-mcp`](https://github.com/farming-labs/farm.js/tree/main/examples/api-mcp).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FarmPlugin, type EndpointMCPMetadata, type RouteSchema } from "@farm.js/core/plugin";
|
|
2
|
+
import type { FarmMCPAuthorization, FarmMCPAuthorizeContext, FarmMCPConfig, FarmMCPStandaloneTool } from "@farm.js/core";
|
|
3
|
+
export interface APIMCPAuthorization extends FarmMCPAuthorization {
|
|
4
|
+
}
|
|
5
|
+
export interface APIMCPAuthorizeContext extends FarmMCPAuthorizeContext {
|
|
6
|
+
}
|
|
7
|
+
/** @deprecated Put `EndpointMCPMetadata` on a route's `mcp` option instead. */
|
|
8
|
+
export interface APIMCPToolOptions extends EndpointMCPMetadata {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
export interface APIMCPOptions extends Omit<FarmMCPConfig, "enabled"> {
|
|
12
|
+
}
|
|
13
|
+
export type { FarmMCPStandaloneTool, FarmMCPExecuteContext } from "@farm.js/core";
|
|
14
|
+
/** Define a server-only MCP tool with schema-inferred execute arguments. */
|
|
15
|
+
export declare function defineTool<Schema extends RouteSchema, Result, OutputSchema extends RouteSchema | undefined = undefined>(definition: FarmMCPStandaloneTool<Schema, Result, OutputSchema>): FarmMCPStandaloneTool<Schema, Result, OutputSchema>;
|
|
16
|
+
/**
|
|
17
|
+
* Mount an experimental Streamable HTTP MCP server with API routes and standalone tools.
|
|
18
|
+
* API endpoints retain their validation and middleware; standalone tools own their schema and execute.
|
|
19
|
+
*/
|
|
20
|
+
export declare function apiMcp(options: APIMCPOptions): FarmPlugin;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,mBAAmB,EAIxB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EAEb,qBAAqB,EAEtB,MAAM,eAAe,CAAC;AAYvB,MAAM,WAAW,mBAAoB,SAAQ,oBAAoB;CAAG;AAEpE,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;CAAG;AAE1E,+EAA+E;AAC/E,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC;CAAG;AAqBxE,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAElF,4EAA4E;AAC5E,wBAAgB,UAAU,CACxB,MAAM,SAAS,WAAW,EAC1B,MAAM,EACN,YAAY,SAAS,WAAW,GAAG,SAAS,GAAG,SAAS,EAExD,UAAU,EAAE,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,GAC9D,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,UAAU,CAuBzD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import { createMcpHandler, fromJsonSchema, McpServer, } from "@modelcontextprotocol/server";
|
|
2
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/server/validators/cf-worker";
|
|
3
|
+
import { parseRouteSchema } from "@farm.js/core/api/runtime";
|
|
4
|
+
import { toJSONSchema } from "zod";
|
|
5
|
+
const TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
6
|
+
const PROTECTED_HEADER = /^(authorization|cookie|set-cookie|proxy-authorization|x-api-key|x-auth-token|x-.*(?:token|secret|key)|connection|keep-alive|te|trailer|transfer-encoding|upgrade|host|origin|referer|forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-cluster-client-ip|x-original-.+|x-rewrite-url|cf-.+|true-client-ip|fastly-.+|x-vercel-.+|sec-fetch-.+|access-control-request-.+)$/i;
|
|
7
|
+
const schemaValidator = new CfWorkerJsonSchemaValidator();
|
|
8
|
+
/** Define a server-only MCP tool with schema-inferred execute arguments. */
|
|
9
|
+
export function defineTool(definition) {
|
|
10
|
+
return definition;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mount an experimental Streamable HTTP MCP server with API routes and standalone tools.
|
|
14
|
+
* API endpoints retain their validation and middleware; standalone tools own their schema and execute.
|
|
15
|
+
*/
|
|
16
|
+
export function apiMcp(options) {
|
|
17
|
+
const normalized = validateOptions(options);
|
|
18
|
+
return {
|
|
19
|
+
name: "farm:api-mcp",
|
|
20
|
+
routes: ({ route, api }) => {
|
|
21
|
+
const tools = api.available ? bindTools(api, normalized) : [];
|
|
22
|
+
const handler = createHandler(normalized, tools);
|
|
23
|
+
const serve = (request, body) => handler(request, body);
|
|
24
|
+
return [
|
|
25
|
+
route.get(normalized.path, {
|
|
26
|
+
handler: (request) => serve(request, undefined),
|
|
27
|
+
}),
|
|
28
|
+
route.post(normalized.path, {
|
|
29
|
+
handler: (request, { input }) => serve(request, input.body),
|
|
30
|
+
}),
|
|
31
|
+
route.delete(normalized.path, {
|
|
32
|
+
handler: (request, { input }) => serve(request, input.body),
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function createHandler(options, tools) {
|
|
39
|
+
const catalog = Object.freeze(tools.map((tool) => Object.freeze({
|
|
40
|
+
name: tool.config.name,
|
|
41
|
+
title: tool.config.title,
|
|
42
|
+
description: tool.config.description,
|
|
43
|
+
...tool.annotations,
|
|
44
|
+
...(tool.kind === "endpoint"
|
|
45
|
+
? { kind: "endpoint", method: tool.route.method, path: tool.route.path }
|
|
46
|
+
: { kind: "standalone" }),
|
|
47
|
+
})));
|
|
48
|
+
const serverIdentity = Object.freeze({
|
|
49
|
+
name: options.name,
|
|
50
|
+
version: options.version,
|
|
51
|
+
path: options.path,
|
|
52
|
+
});
|
|
53
|
+
const toolNames = new Set(catalog.map(({ name }) => name));
|
|
54
|
+
const mcp = createMcpHandler(({ requestInfo, authInfo }) => {
|
|
55
|
+
const server = new McpServer({ name: options.name, version: options.version }, { capabilities: { tools: {} } });
|
|
56
|
+
const allowed = authInfo?.extra?.farmAllowedTools;
|
|
57
|
+
const allowedNames = allowed === undefined ? undefined : new Set(allowed);
|
|
58
|
+
// A fresh server is created per HTTP request. Unregistered tools are neither
|
|
59
|
+
// discoverable nor callable, regardless of the incoming protocol method.
|
|
60
|
+
for (const tool of tools) {
|
|
61
|
+
if (allowedNames && !allowedNames.has(tool.config.name))
|
|
62
|
+
continue;
|
|
63
|
+
server.registerTool(tool.config.name, {
|
|
64
|
+
title: tool.config.title,
|
|
65
|
+
description: tool.config.description,
|
|
66
|
+
inputSchema: fromJsonSchema(tool.inputSchema, schemaValidator),
|
|
67
|
+
...(tool.outputSchema === undefined
|
|
68
|
+
? {}
|
|
69
|
+
: {
|
|
70
|
+
outputSchema: fromJsonSchema(tool.outputSchema, schemaValidator),
|
|
71
|
+
}),
|
|
72
|
+
annotations: tool.annotations,
|
|
73
|
+
}, async (input, context) => {
|
|
74
|
+
const request = context.http?.req ?? requestInfo;
|
|
75
|
+
if (tool.kind === "endpoint") {
|
|
76
|
+
return invokeTool(tool, input, request, context.mcpReq.signal);
|
|
77
|
+
}
|
|
78
|
+
const authorization = authInfo?.extra?.farmAuthorization;
|
|
79
|
+
if (!request || !authorization)
|
|
80
|
+
return toolError("Missing MCP authorization context.");
|
|
81
|
+
return invokeStandaloneTool(tool, input, {
|
|
82
|
+
request,
|
|
83
|
+
authorization,
|
|
84
|
+
signal: AbortSignal.any([request.signal, context.mcpReq.signal]),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return server;
|
|
89
|
+
});
|
|
90
|
+
return async (request, parsedBody) => {
|
|
91
|
+
let authorization;
|
|
92
|
+
if (options.authorize) {
|
|
93
|
+
const messages = Array.isArray(parsedBody) && parsedBody.length > 0 ? parsedBody : [parsedBody];
|
|
94
|
+
let first;
|
|
95
|
+
let denied = false;
|
|
96
|
+
let inconsistent = false;
|
|
97
|
+
for (const message of messages) {
|
|
98
|
+
const result = await options.authorize({
|
|
99
|
+
request,
|
|
100
|
+
tool: getCalledTool(message),
|
|
101
|
+
tools: catalog,
|
|
102
|
+
server: serverIdentity,
|
|
103
|
+
});
|
|
104
|
+
if (result === false) {
|
|
105
|
+
denied = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!result || typeof result.subject !== "string" || result.subject.length === 0) {
|
|
109
|
+
throw new TypeError("apiMcp authorize must return false or an object with a subject string.");
|
|
110
|
+
}
|
|
111
|
+
if (first && !sameAuthorizationIdentity(first, result)) {
|
|
112
|
+
inconsistent = true;
|
|
113
|
+
}
|
|
114
|
+
first ??= result;
|
|
115
|
+
}
|
|
116
|
+
if (denied || inconsistent)
|
|
117
|
+
return unauthorized();
|
|
118
|
+
authorization = first;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
authorization = { subject: "anonymous" };
|
|
122
|
+
}
|
|
123
|
+
return mcp.fetch(request, {
|
|
124
|
+
parsedBody,
|
|
125
|
+
authInfo: toMCPAuthInfo(authorization, resolveAllowedTools(authorization, toolNames)),
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function invokeTool(tool, input, sourceRequest, signal) {
|
|
130
|
+
try {
|
|
131
|
+
const params = asRecord(input.params, "params");
|
|
132
|
+
const pathname = interpolatePath(tool.route.path, params);
|
|
133
|
+
const origin = sourceRequest ? new URL(sourceRequest.url).origin : "http://farm.local";
|
|
134
|
+
const url = new URL(pathname, origin);
|
|
135
|
+
appendQuery(url, asRecord(input.query, "query"));
|
|
136
|
+
const headers = createForwardedHeaders(sourceRequest?.headers, asRecord(input.headers, "headers"));
|
|
137
|
+
const body = input.body === undefined ? undefined : JSON.stringify(input.body);
|
|
138
|
+
if (body !== undefined)
|
|
139
|
+
headers.set("content-type", "application/json");
|
|
140
|
+
const request = new Request(url, {
|
|
141
|
+
method: tool.route.method,
|
|
142
|
+
headers,
|
|
143
|
+
body: tool.route.method === "GET" || tool.route.method === "HEAD" ? undefined : body,
|
|
144
|
+
signal,
|
|
145
|
+
});
|
|
146
|
+
const routeParams = Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
147
|
+
key,
|
|
148
|
+
Array.isArray(value) ? value.map(String) : String(value),
|
|
149
|
+
]));
|
|
150
|
+
const response = await tool.route.invoke(request, routeParams);
|
|
151
|
+
const mediaType = response.headers.get("content-type")?.split(";", 1)[0].trim() ?? "";
|
|
152
|
+
if (response.status !== 204 &&
|
|
153
|
+
mediaType !== "application/json" &&
|
|
154
|
+
!mediaType.endsWith("+json")) {
|
|
155
|
+
return toolError(`${tool.key} returned ${mediaType || "a response without a Content-Type"}; MCP API tools must return JSON.`);
|
|
156
|
+
}
|
|
157
|
+
const result = response.status === 204 ? null : await response.json();
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
return toolError(JSON.stringify({ status: response.status, error: result }));
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
163
|
+
structuredContent: { result },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return toolError(error instanceof Error ? error.message : "Farm API tool invocation failed.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function invokeStandaloneTool(tool, input, context) {
|
|
171
|
+
try {
|
|
172
|
+
context.signal.throwIfAborted();
|
|
173
|
+
// JSON Schema advertises the wire shape; the original validator owns async
|
|
174
|
+
// refinements, defaults, and transforms, just as it does for API endpoints.
|
|
175
|
+
const parsed = await parseRouteSchema(tool.definition.inputSchema, input);
|
|
176
|
+
context.signal.throwIfAborted();
|
|
177
|
+
let result = await tool.definition.execute(parsed, context);
|
|
178
|
+
context.signal.throwIfAborted();
|
|
179
|
+
if (tool.definition.outputSchema !== undefined) {
|
|
180
|
+
try {
|
|
181
|
+
result = await parseRouteSchema(tool.definition.outputSchema, result);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Output can contain private application data. Do not return validator
|
|
185
|
+
// issues or custom refinement messages to the caller.
|
|
186
|
+
return toolError(`MCP tool "${tool.config.name}" returned a value that does not match its output schema.`);
|
|
187
|
+
}
|
|
188
|
+
context.signal.throwIfAborted();
|
|
189
|
+
}
|
|
190
|
+
const text = JSON.stringify(result, (_key, value) => {
|
|
191
|
+
if (typeof value === "bigint" ||
|
|
192
|
+
typeof value === "function" ||
|
|
193
|
+
typeof value === "symbol" ||
|
|
194
|
+
(typeof value === "number" && !Number.isFinite(value)) ||
|
|
195
|
+
(value !== null &&
|
|
196
|
+
typeof value === "object" &&
|
|
197
|
+
!Array.isArray(value) &&
|
|
198
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
199
|
+
Object.getPrototypeOf(value) !== null))
|
|
200
|
+
throw new TypeError("Standalone MCP tools must return JSON-serializable data, not a Response or stream.");
|
|
201
|
+
return value;
|
|
202
|
+
});
|
|
203
|
+
if (text === undefined)
|
|
204
|
+
throw new TypeError("Standalone MCP tools must return JSON-serializable data.");
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text }],
|
|
207
|
+
structuredContent: { result: JSON.parse(text) },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return toolError(error instanceof Error ? error.message : "Farm MCP tool execution failed.");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function bindTools(api, options) {
|
|
215
|
+
const selected = [];
|
|
216
|
+
const endpoints = new Set();
|
|
217
|
+
const select = (endpoint, metadata) => {
|
|
218
|
+
if (endpoint.path === options.path) {
|
|
219
|
+
throw new TypeError(`apiMcp cannot expose its own route (${endpoint.method} ${endpoint.path}).`);
|
|
220
|
+
}
|
|
221
|
+
if (endpoint.method === "OPTIONS") {
|
|
222
|
+
throw new TypeError(`apiMcp cannot expose OPTIONS route ${endpoint.path}; use an application operation instead.`);
|
|
223
|
+
}
|
|
224
|
+
const key = `${endpoint.method} ${endpoint.path}`;
|
|
225
|
+
if (endpoints.has(key))
|
|
226
|
+
throw new TypeError(`apiMcp endpoint ${key} is selected more than once.`);
|
|
227
|
+
endpoints.add(key);
|
|
228
|
+
const config = { ...(endpoint.mcp === true ? {} : endpoint.mcp), ...metadata };
|
|
229
|
+
const resolved = {
|
|
230
|
+
...config,
|
|
231
|
+
name: config.name ?? deriveToolName(endpoint.method, endpoint.path),
|
|
232
|
+
};
|
|
233
|
+
selected.push({
|
|
234
|
+
kind: "endpoint",
|
|
235
|
+
key,
|
|
236
|
+
route: endpoint,
|
|
237
|
+
config: resolved,
|
|
238
|
+
inputSchema: createToolInputSchema(endpoint),
|
|
239
|
+
outputSchema: createToolOutputSchema(endpoint.output),
|
|
240
|
+
annotations: createAnnotations(endpoint.method, resolved),
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
if (options.tools === undefined) {
|
|
244
|
+
for (const endpoint of api.list()) {
|
|
245
|
+
if (endpoint.mcp)
|
|
246
|
+
select(endpoint);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
for (const definition of options.tools) {
|
|
251
|
+
if (definition && typeof definition === "object" && "execute" in definition) {
|
|
252
|
+
if ("endpoint" in definition ||
|
|
253
|
+
typeof definition.execute !== "function" ||
|
|
254
|
+
!definition.inputSchema) {
|
|
255
|
+
throw new TypeError("Standalone MCP tools need inputSchema and execute, without an endpoint reference.");
|
|
256
|
+
}
|
|
257
|
+
const config = { ...definition, name: definition.name };
|
|
258
|
+
selected.push({
|
|
259
|
+
kind: "standalone",
|
|
260
|
+
key: `tool ${definition.name}`,
|
|
261
|
+
definition,
|
|
262
|
+
config,
|
|
263
|
+
inputSchema: requireObjectSchema(toToolJSONSchema(definition.inputSchema, "inputSchema"), "inputSchema"),
|
|
264
|
+
outputSchema: createToolOutputSchema(definition.outputSchema),
|
|
265
|
+
annotations: createAnnotations(undefined, config),
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const { endpoint, ...metadata } = typeof definition === "function" ? { endpoint: definition } : (definition ?? {});
|
|
270
|
+
if (typeof endpoint !== "function" ||
|
|
271
|
+
!endpoint.__types ||
|
|
272
|
+
typeof endpoint.__path !== "string" ||
|
|
273
|
+
!endpoint.__path.startsWith("/api/") ||
|
|
274
|
+
typeof endpoint.__method !== "string") {
|
|
275
|
+
throw new TypeError("apiMcp tools need a createEndpoint reference with an explicit /api path; use createEndpoint('/api/...', options, handler), or keep mcp metadata on a path-inferred route.");
|
|
276
|
+
}
|
|
277
|
+
if (endpoint.__path === options.path) {
|
|
278
|
+
throw new TypeError(`apiMcp cannot expose its own route (${endpoint.__method} ${endpoint.__path}).`);
|
|
279
|
+
}
|
|
280
|
+
// Config and route modules may be independently evaluated. Resolve the
|
|
281
|
+
// mounted endpoint by its declared contract, never by function identity.
|
|
282
|
+
const mounted = api.get(endpoint.__method, endpoint.__path);
|
|
283
|
+
if (!mounted)
|
|
284
|
+
throw new TypeError(`apiMcp ${endpoint.__method} ${endpoint.__path} is not a mounted app endpoint.`);
|
|
285
|
+
select(mounted, metadata);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (selected.length === 0) {
|
|
289
|
+
throw new TypeError("apiMcp needs at least one endpoint or standalone tool selected in tools, or route-owned MCP metadata.");
|
|
290
|
+
}
|
|
291
|
+
const names = new Set();
|
|
292
|
+
for (const { key, config } of selected) {
|
|
293
|
+
if (typeof config.name !== "string" || !TOOL_NAME.test(config.name)) {
|
|
294
|
+
throw new TypeError(`apiMcp tool for "${key}" needs a valid MCP name (1-128 letters, numbers, dots, underscores, or hyphens).`);
|
|
295
|
+
}
|
|
296
|
+
if (names.has(config.name))
|
|
297
|
+
throw new TypeError(`apiMcp tool name "${config.name}" is duplicated.`);
|
|
298
|
+
names.add(config.name);
|
|
299
|
+
}
|
|
300
|
+
return selected;
|
|
301
|
+
}
|
|
302
|
+
function deriveToolName(method, path) {
|
|
303
|
+
const parts = [method.toLowerCase()];
|
|
304
|
+
for (const segment of path.split("/").slice(2)) {
|
|
305
|
+
const parameter = /^\[{1,2}(?:\.\.\.)?([^\]]+)\]{1,2}$/.exec(segment)?.[1];
|
|
306
|
+
const source = parameter ?? segment;
|
|
307
|
+
const normalized = source
|
|
308
|
+
.normalize("NFKD")
|
|
309
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
310
|
+
.replace(/^_+|_+$/g, "")
|
|
311
|
+
.toLowerCase();
|
|
312
|
+
if (!normalized) {
|
|
313
|
+
throw new TypeError(`apiMcp cannot derive a tool name for ${method} ${path}; set mcp: { name: "..." }.`);
|
|
314
|
+
}
|
|
315
|
+
if (parameter)
|
|
316
|
+
parts.push("by");
|
|
317
|
+
parts.push(normalized);
|
|
318
|
+
}
|
|
319
|
+
if (parts.length === 1)
|
|
320
|
+
parts.push("api");
|
|
321
|
+
const name = parts.join("_");
|
|
322
|
+
if (!TOOL_NAME.test(name)) {
|
|
323
|
+
throw new TypeError(`apiMcp cannot derive a valid tool name for ${method} ${path}; set mcp: { name: "..." }.`);
|
|
324
|
+
}
|
|
325
|
+
return name;
|
|
326
|
+
}
|
|
327
|
+
function createToolInputSchema(endpoint) {
|
|
328
|
+
const properties = {};
|
|
329
|
+
const required = [];
|
|
330
|
+
const pathParams = createPathParamsSchema(endpoint.path, endpoint.input.params);
|
|
331
|
+
if (pathParams) {
|
|
332
|
+
properties.params = pathParams;
|
|
333
|
+
if (pathParams.required?.length)
|
|
334
|
+
required.push("params");
|
|
335
|
+
}
|
|
336
|
+
if (endpoint.input.query) {
|
|
337
|
+
const query = requireObjectSchema(toToolJSONSchema(endpoint.input.query, "query"), "query");
|
|
338
|
+
properties.query = query;
|
|
339
|
+
if (query.required?.length)
|
|
340
|
+
required.push("query");
|
|
341
|
+
}
|
|
342
|
+
if (endpoint.input.body) {
|
|
343
|
+
properties.body = toToolJSONSchema(endpoint.input.body, "body");
|
|
344
|
+
required.push("body");
|
|
345
|
+
}
|
|
346
|
+
if (endpoint.input.headers) {
|
|
347
|
+
const headers = safeHeaderSchema(requireObjectSchema(toToolJSONSchema(endpoint.input.headers, "headers"), "headers"));
|
|
348
|
+
if (Object.keys(headers.properties ?? {}).length > 0) {
|
|
349
|
+
properties.headers = headers;
|
|
350
|
+
if (headers.required?.length)
|
|
351
|
+
required.push("headers");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties,
|
|
357
|
+
...(required.length ? { required } : {}),
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function createPathParamsSchema(path, schema) {
|
|
362
|
+
const segments = [
|
|
363
|
+
...path.matchAll(/\/(?:\[\[\.\.\.([^\]]+)\]\]|\[\.\.\.([^\]]+)\]|\[([^\]]+)\])/g),
|
|
364
|
+
];
|
|
365
|
+
if (segments.length === 0)
|
|
366
|
+
return undefined;
|
|
367
|
+
if (schema)
|
|
368
|
+
return requireObjectSchema(toToolJSONSchema(schema, "params"), "params");
|
|
369
|
+
const properties = {};
|
|
370
|
+
const required = [];
|
|
371
|
+
for (const match of segments) {
|
|
372
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
373
|
+
const catchAll = Boolean(match[1] || match[2]);
|
|
374
|
+
properties[name] = catchAll
|
|
375
|
+
? { type: "array", items: { type: "string" }, minItems: match[1] ? 0 : 1 }
|
|
376
|
+
: { type: "string" };
|
|
377
|
+
if (!match[1])
|
|
378
|
+
required.push(name);
|
|
379
|
+
}
|
|
380
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
381
|
+
}
|
|
382
|
+
function toToolJSONSchema(schema, location, io = "input") {
|
|
383
|
+
let result;
|
|
384
|
+
try {
|
|
385
|
+
const standardJSON = schema["~standard"]?.jsonSchema?.[io];
|
|
386
|
+
result =
|
|
387
|
+
typeof standardJSON === "function"
|
|
388
|
+
? standardJSON({ target: "draft-2020-12" })
|
|
389
|
+
: toJSONSchema(schema, { io });
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const reason = error instanceof Error ? ` ${error.message}` : "";
|
|
393
|
+
throw new TypeError(`apiMcp cannot represent the selected tool's ${location} schema.${reason}`);
|
|
394
|
+
}
|
|
395
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
396
|
+
throw new TypeError(`apiMcp cannot represent the selected tool's ${location} schema.`);
|
|
397
|
+
}
|
|
398
|
+
const json = { ...result };
|
|
399
|
+
delete json.$schema;
|
|
400
|
+
return json;
|
|
401
|
+
}
|
|
402
|
+
function createToolOutputSchema(schema) {
|
|
403
|
+
if (schema === undefined)
|
|
404
|
+
return undefined;
|
|
405
|
+
return {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: { result: nestOutputSchema(toToolJSONSchema(schema, "outputSchema", "output")) },
|
|
408
|
+
required: ["result"],
|
|
409
|
+
additionalProperties: false,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** Keep document-local references pointing at the result schema, not the new envelope. */
|
|
413
|
+
function nestOutputSchema(schema) {
|
|
414
|
+
// An explicit resource ID already establishes its own reference scope.
|
|
415
|
+
if (typeof schema.$id === "string")
|
|
416
|
+
return schema;
|
|
417
|
+
const result = { ...schema };
|
|
418
|
+
for (const keyword of ["$ref", "$dynamicRef"]) {
|
|
419
|
+
const reference = result[keyword];
|
|
420
|
+
if (typeof reference === "string" && (reference === "#" || reference.startsWith("#/"))) {
|
|
421
|
+
result[keyword] = `#/properties/result${reference.slice(1)}`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const nest = (value) => value && typeof value === "object" && !Array.isArray(value)
|
|
425
|
+
? nestOutputSchema(value)
|
|
426
|
+
: value;
|
|
427
|
+
// Visit schema positions only: const/default/examples can contain literal $ref keys.
|
|
428
|
+
for (const keyword of [
|
|
429
|
+
"$defs",
|
|
430
|
+
"definitions",
|
|
431
|
+
"properties",
|
|
432
|
+
"patternProperties",
|
|
433
|
+
"dependentSchemas",
|
|
434
|
+
"dependencies",
|
|
435
|
+
]) {
|
|
436
|
+
const children = result[keyword];
|
|
437
|
+
if (children && typeof children === "object" && !Array.isArray(children)) {
|
|
438
|
+
result[keyword] = Object.fromEntries(Object.entries(children).map(([key, child]) => [key, nest(child)]));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const keyword of [
|
|
442
|
+
"items",
|
|
443
|
+
"prefixItems",
|
|
444
|
+
"allOf",
|
|
445
|
+
"anyOf",
|
|
446
|
+
"oneOf",
|
|
447
|
+
"additionalItems",
|
|
448
|
+
"additionalProperties",
|
|
449
|
+
"unevaluatedItems",
|
|
450
|
+
"unevaluatedProperties",
|
|
451
|
+
"propertyNames",
|
|
452
|
+
"contains",
|
|
453
|
+
"not",
|
|
454
|
+
"if",
|
|
455
|
+
"then",
|
|
456
|
+
"else",
|
|
457
|
+
"contentSchema",
|
|
458
|
+
]) {
|
|
459
|
+
if (keyword in result) {
|
|
460
|
+
const child = result[keyword];
|
|
461
|
+
result[keyword] = Array.isArray(child) ? child.map(nest) : nest(child);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
function requireObjectSchema(schema, location) {
|
|
467
|
+
if (schema.type !== "object" || !schema.properties || typeof schema.properties !== "object") {
|
|
468
|
+
throw new TypeError(`apiMcp ${location} schemas must describe an object.`);
|
|
469
|
+
}
|
|
470
|
+
return schema;
|
|
471
|
+
}
|
|
472
|
+
function safeHeaderSchema(schema) {
|
|
473
|
+
const properties = Object.fromEntries(Object.entries(schema.properties).filter(([name]) => !PROTECTED_HEADER.test(name)));
|
|
474
|
+
const required = (schema.required ?? []).filter((name) => !PROTECTED_HEADER.test(name));
|
|
475
|
+
const safe = {
|
|
476
|
+
...schema,
|
|
477
|
+
properties,
|
|
478
|
+
additionalProperties: false,
|
|
479
|
+
};
|
|
480
|
+
if (required.length)
|
|
481
|
+
safe.required = required;
|
|
482
|
+
else
|
|
483
|
+
delete safe.required;
|
|
484
|
+
return safe;
|
|
485
|
+
}
|
|
486
|
+
function interpolatePath(path, params) {
|
|
487
|
+
return path
|
|
488
|
+
.split("/")
|
|
489
|
+
.map((segment) => {
|
|
490
|
+
const optional = /^\[\[\.\.\.(.+)\]\]$/.exec(segment);
|
|
491
|
+
const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
|
|
492
|
+
const dynamic = /^\[(.+)\]$/.exec(segment);
|
|
493
|
+
const name = optional?.[1] ?? catchAll?.[1] ?? dynamic?.[1];
|
|
494
|
+
if (!name)
|
|
495
|
+
return segment;
|
|
496
|
+
const value = params[name];
|
|
497
|
+
if (optional && (value === undefined || (Array.isArray(value) && value.length === 0)))
|
|
498
|
+
return "";
|
|
499
|
+
if (value === undefined)
|
|
500
|
+
throw new TypeError(`Missing route parameter "${name}".`);
|
|
501
|
+
if (optional || catchAll) {
|
|
502
|
+
if (!Array.isArray(value))
|
|
503
|
+
throw new TypeError(`Route parameter "${name}" must be an array.`);
|
|
504
|
+
return value.map((part) => encodeURIComponent(String(part))).join("/");
|
|
505
|
+
}
|
|
506
|
+
if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
|
|
507
|
+
throw new TypeError(`Route parameter "${name}" must be a scalar value.`);
|
|
508
|
+
}
|
|
509
|
+
return encodeURIComponent(String(value));
|
|
510
|
+
})
|
|
511
|
+
.filter(Boolean)
|
|
512
|
+
.join("/")
|
|
513
|
+
.replace(/^/, "/");
|
|
514
|
+
}
|
|
515
|
+
function appendQuery(url, query) {
|
|
516
|
+
for (const [key, value] of Object.entries(query)) {
|
|
517
|
+
const values = Array.isArray(value) ? value : [value];
|
|
518
|
+
for (const entry of values) {
|
|
519
|
+
if (entry === undefined)
|
|
520
|
+
continue;
|
|
521
|
+
if (entry !== null && typeof entry === "object") {
|
|
522
|
+
throw new TypeError(`Query parameter "${key}" must be a scalar value or an array.`);
|
|
523
|
+
}
|
|
524
|
+
url.searchParams.append(key, String(entry));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function createForwardedHeaders(source, supplied) {
|
|
529
|
+
const headers = new Headers(source);
|
|
530
|
+
headers.delete("content-length");
|
|
531
|
+
headers.delete("content-type");
|
|
532
|
+
for (const name of Array.from(headers.keys())) {
|
|
533
|
+
if (name.toLowerCase().startsWith("mcp-"))
|
|
534
|
+
headers.delete(name);
|
|
535
|
+
}
|
|
536
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
537
|
+
if (PROTECTED_HEADER.test(name)) {
|
|
538
|
+
throw new TypeError(`Tool input cannot override protected header "${name}".`);
|
|
539
|
+
}
|
|
540
|
+
if (typeof value !== "string")
|
|
541
|
+
throw new TypeError(`Header "${name}" must be a string.`);
|
|
542
|
+
headers.set(name, value);
|
|
543
|
+
}
|
|
544
|
+
return headers;
|
|
545
|
+
}
|
|
546
|
+
function asRecord(value, location) {
|
|
547
|
+
if (value === undefined)
|
|
548
|
+
return {};
|
|
549
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
550
|
+
throw new TypeError(`Tool ${location} must be an object.`);
|
|
551
|
+
}
|
|
552
|
+
return value;
|
|
553
|
+
}
|
|
554
|
+
function createAnnotations(method, options) {
|
|
555
|
+
const readOnly = method === "GET" || method === "HEAD" || method === "QUERY";
|
|
556
|
+
return {
|
|
557
|
+
readOnlyHint: options.readOnlyHint ?? readOnly,
|
|
558
|
+
destructiveHint: options.destructiveHint ?? (method === undefined || method === "DELETE"),
|
|
559
|
+
...(options.idempotentHint === undefined ? {} : { idempotentHint: options.idempotentHint }),
|
|
560
|
+
...(options.openWorldHint === undefined ? {} : { openWorldHint: options.openWorldHint }),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function getCalledTool(body) {
|
|
564
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
565
|
+
return undefined;
|
|
566
|
+
const request = body;
|
|
567
|
+
return request.method === "tools/call" && typeof request.params?.name === "string"
|
|
568
|
+
? request.params.name
|
|
569
|
+
: undefined;
|
|
570
|
+
}
|
|
571
|
+
function sameAuthorizationIdentity(left, right) {
|
|
572
|
+
return (left.subject === right.subject &&
|
|
573
|
+
sameAuthorizationList(left.scopes, right.scopes) &&
|
|
574
|
+
sameAuthorizationList(left.tools, right.tools));
|
|
575
|
+
}
|
|
576
|
+
function sameAuthorizationList(left, right) {
|
|
577
|
+
if (left === undefined || right === undefined)
|
|
578
|
+
return left === right;
|
|
579
|
+
if (!Array.isArray(left) || !Array.isArray(right))
|
|
580
|
+
return false;
|
|
581
|
+
const leftSet = new Set(left);
|
|
582
|
+
const rightSet = new Set(right);
|
|
583
|
+
return (leftSet.size === rightSet.size && Array.from(leftSet).every((value) => rightSet.has(value)));
|
|
584
|
+
}
|
|
585
|
+
function resolveAllowedTools(authorization, names) {
|
|
586
|
+
if (authorization.tools === undefined)
|
|
587
|
+
return undefined;
|
|
588
|
+
if (!Array.isArray(authorization.tools) ||
|
|
589
|
+
authorization.tools.some((name) => typeof name !== "string" || !names.has(name))) {
|
|
590
|
+
throw new TypeError("apiMcp authorize tools must be an array of configured tool names.");
|
|
591
|
+
}
|
|
592
|
+
return [...authorization.tools];
|
|
593
|
+
}
|
|
594
|
+
function toMCPAuthInfo(authorization, allowedTools) {
|
|
595
|
+
return {
|
|
596
|
+
token: "farm-authorized",
|
|
597
|
+
clientId: authorization.subject,
|
|
598
|
+
scopes: Array.isArray(authorization.scopes) ? authorization.scopes : [],
|
|
599
|
+
extra: { farmAuthorization: authorization, farmAllowedTools: allowedTools },
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function unauthorized() {
|
|
603
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
604
|
+
status: 401,
|
|
605
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function toolError(message) {
|
|
609
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
610
|
+
}
|
|
611
|
+
function validateOptions(options) {
|
|
612
|
+
if (!options || typeof options !== "object")
|
|
613
|
+
throw new TypeError("apiMcp options are required.");
|
|
614
|
+
if (options.tools !== undefined && !Array.isArray(options.tools)) {
|
|
615
|
+
throw new TypeError("apiMcp tools must be an array of endpoint references, { endpoint, ...metadata } declarations, or defineTool() tools, not a method/path map.");
|
|
616
|
+
}
|
|
617
|
+
if (!options.authorize && options.allowUnauthenticated !== true) {
|
|
618
|
+
throw new TypeError("apiMcp requires authorize; set allowUnauthenticated: true only for a deliberately public server.");
|
|
619
|
+
}
|
|
620
|
+
const path = options.path ?? "/api/mcp";
|
|
621
|
+
if (!/^\/api\/[^/?#]+(?:\/[^/?#]+)*$/.test(path)) {
|
|
622
|
+
throw new TypeError("apiMcp path must be a canonical /api path.");
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
...options,
|
|
626
|
+
path,
|
|
627
|
+
name: options.name ?? "farm-api",
|
|
628
|
+
version: options.version ?? "1.0.0",
|
|
629
|
+
};
|
|
630
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FarmMCPConfig } from "@farm.js/core";
|
|
2
|
+
/** @internal Loaded by Farm when the top-level `mcp` config is enabled. */
|
|
3
|
+
export declare function createFarmMCPPlugin(config: FarmMCPConfig): import("@farm.js/core").FarmPlugin<any, Record<string, unknown>, any, any, unknown, false, import("@farm.js/core").PluginRoutes>;
|
|
4
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,oIAGxD"}
|
package/dist/internal.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Compose Farm.js API routes and standalone tools in one MCP server",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"farm.js",
|
|
8
|
+
"mcp"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/farming-labs/farm.js",
|
|
14
|
+
"directory": "packages/farm-mcp"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./internal": {
|
|
29
|
+
"types": "./dist/internal.d.ts",
|
|
30
|
+
"import": "./dist/internal.js",
|
|
31
|
+
"default": "./dist/internal.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
39
|
+
"zod": "^4.1.12",
|
|
40
|
+
"@farm.js/core": "0.1.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.10.5",
|
|
44
|
+
"typescript": "^5.3.3",
|
|
45
|
+
"vitest": "^3.2.7"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.13.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
52
|
+
"dev": "tsc --watch",
|
|
53
|
+
"type-check": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
}
|
|
6
56
|
}
|