@proofkit/fmdapi 5.0.3-beta.1 → 5.1.0-beta.3
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/bin/intent.js +20 -0
- package/dist/esm/adapters/fetch-base.js.map +1 -1
- package/dist/esm/adapters/fetch.js.map +1 -1
- package/dist/esm/adapters/fm-mcp.d.ts +32 -0
- package/dist/esm/adapters/fm-mcp.js +170 -0
- package/dist/esm/adapters/fm-mcp.js.map +1 -0
- package/dist/esm/adapters/otto.js.map +1 -1
- package/dist/esm/client-types.js.map +1 -1
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tokenStore/memory.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +30 -17
- package/skills/fmdapi-client/SKILL.md +490 -0
- package/src/adapters/fm-mcp.ts +224 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateResponse,
|
|
3
|
+
DeleteResponse,
|
|
4
|
+
GetResponse,
|
|
5
|
+
LayoutMetadataResponse,
|
|
6
|
+
RawFMResponse,
|
|
7
|
+
ScriptResponse,
|
|
8
|
+
UpdateResponse,
|
|
9
|
+
} from "../client-types.js";
|
|
10
|
+
import { FileMakerError } from "../client-types.js";
|
|
11
|
+
import type {
|
|
12
|
+
Adapter,
|
|
13
|
+
CreateOptions,
|
|
14
|
+
DeleteOptions,
|
|
15
|
+
ExecuteScriptOptions,
|
|
16
|
+
FindOptions,
|
|
17
|
+
GetOptions,
|
|
18
|
+
LayoutMetadataOptions,
|
|
19
|
+
ListOptions,
|
|
20
|
+
UpdateOptions,
|
|
21
|
+
} from "./core.js";
|
|
22
|
+
|
|
23
|
+
const TRAILING_SLASHES_REGEX = /\/+$/;
|
|
24
|
+
|
|
25
|
+
export interface FmMcpAdapterOptions {
|
|
26
|
+
/** Base URL of the local FM MCP server (e.g. "http://localhost:3000") */
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
/** Name of the connected FileMaker file */
|
|
29
|
+
connectedFileName: string;
|
|
30
|
+
/** Name of the FM script that executes Data API calls. Defaults to "execute_data_api" */
|
|
31
|
+
scriptName?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class FmMcpAdapter implements Adapter {
|
|
35
|
+
protected baseUrl: string;
|
|
36
|
+
protected connectedFileName: string;
|
|
37
|
+
protected scriptName: string;
|
|
38
|
+
|
|
39
|
+
constructor(options: FmMcpAdapterOptions) {
|
|
40
|
+
this.baseUrl = options.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
41
|
+
this.connectedFileName = options.connectedFileName;
|
|
42
|
+
this.scriptName = options.scriptName ?? "execute_data_api";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected request = async (params: {
|
|
46
|
+
layout: string;
|
|
47
|
+
body: object;
|
|
48
|
+
action?: "read" | "metaData" | "create" | "update" | "delete";
|
|
49
|
+
timeout?: number;
|
|
50
|
+
fetchOptions?: RequestInit;
|
|
51
|
+
}): Promise<unknown> => {
|
|
52
|
+
const { action = "read", layout, body, fetchOptions = {} } = params;
|
|
53
|
+
|
|
54
|
+
// Normalize underscore-prefixed keys to match FM script expectations
|
|
55
|
+
const normalizedBody: Record<string, unknown> = { ...body } as Record<string, unknown>;
|
|
56
|
+
if ("_offset" in normalizedBody) {
|
|
57
|
+
normalizedBody.offset = normalizedBody._offset;
|
|
58
|
+
normalizedBody._offset = undefined;
|
|
59
|
+
}
|
|
60
|
+
if ("_limit" in normalizedBody) {
|
|
61
|
+
normalizedBody.limit = normalizedBody._limit;
|
|
62
|
+
normalizedBody._limit = undefined;
|
|
63
|
+
}
|
|
64
|
+
if ("_sort" in normalizedBody) {
|
|
65
|
+
normalizedBody.sort = normalizedBody._sort;
|
|
66
|
+
normalizedBody._sort = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const scriptParam = JSON.stringify({
|
|
70
|
+
...normalizedBody,
|
|
71
|
+
layouts: layout,
|
|
72
|
+
action,
|
|
73
|
+
version: "vLatest",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
78
|
+
if (params.timeout) {
|
|
79
|
+
timeout = setTimeout(() => controller.abort(), params.timeout);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const headers = new Headers(fetchOptions?.headers);
|
|
83
|
+
headers.set("Content-Type", "application/json");
|
|
84
|
+
|
|
85
|
+
let res: Response;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetch(`${this.baseUrl}/callScript`, {
|
|
88
|
+
...fetchOptions,
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers,
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
connectedFileName: this.connectedFileName,
|
|
93
|
+
scriptName: this.scriptName,
|
|
94
|
+
data: scriptParam,
|
|
95
|
+
}),
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
if (timeout) {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new FileMakerError(String(res.status), `FM MCP request failed (${res.status}): ${await res.text()}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const raw = await res.json();
|
|
109
|
+
// The /callScript response wraps the script result as a string or object
|
|
110
|
+
let scriptResult: unknown;
|
|
111
|
+
try {
|
|
112
|
+
scriptResult = typeof raw.result === "string" ? JSON.parse(raw.result) : (raw.result ?? raw);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new FileMakerError(
|
|
115
|
+
"500",
|
|
116
|
+
`FM MCP response parse failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const respData = scriptResult as RawFMResponse;
|
|
121
|
+
|
|
122
|
+
const errorCode = respData.messages?.[0]?.code;
|
|
123
|
+
if (errorCode !== undefined && errorCode !== "0") {
|
|
124
|
+
throw new FileMakerError(
|
|
125
|
+
errorCode,
|
|
126
|
+
`Filemaker Data API failed with (${errorCode}): ${JSON.stringify(respData, null, 2)}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return respData.response;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
list = async (opts: ListOptions): Promise<GetResponse> => {
|
|
134
|
+
return (await this.request({
|
|
135
|
+
body: opts.data,
|
|
136
|
+
layout: opts.layout,
|
|
137
|
+
timeout: opts.timeout,
|
|
138
|
+
fetchOptions: opts.fetch,
|
|
139
|
+
})) as GetResponse;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
get = async (opts: GetOptions): Promise<GetResponse> => {
|
|
143
|
+
return (await this.request({
|
|
144
|
+
body: opts.data,
|
|
145
|
+
layout: opts.layout,
|
|
146
|
+
timeout: opts.timeout,
|
|
147
|
+
fetchOptions: opts.fetch,
|
|
148
|
+
})) as GetResponse;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
find = async (opts: FindOptions): Promise<GetResponse> => {
|
|
152
|
+
return (await this.request({
|
|
153
|
+
body: opts.data,
|
|
154
|
+
layout: opts.layout,
|
|
155
|
+
timeout: opts.timeout,
|
|
156
|
+
fetchOptions: opts.fetch,
|
|
157
|
+
})) as GetResponse;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
create = async (opts: CreateOptions): Promise<CreateResponse> => {
|
|
161
|
+
return (await this.request({
|
|
162
|
+
action: "create",
|
|
163
|
+
body: opts.data,
|
|
164
|
+
layout: opts.layout,
|
|
165
|
+
timeout: opts.timeout,
|
|
166
|
+
fetchOptions: opts.fetch,
|
|
167
|
+
})) as CreateResponse;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
update = async (opts: UpdateOptions): Promise<UpdateResponse> => {
|
|
171
|
+
return (await this.request({
|
|
172
|
+
action: "update",
|
|
173
|
+
body: opts.data,
|
|
174
|
+
layout: opts.layout,
|
|
175
|
+
timeout: opts.timeout,
|
|
176
|
+
fetchOptions: opts.fetch,
|
|
177
|
+
})) as UpdateResponse;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
delete = async (opts: DeleteOptions): Promise<DeleteResponse> => {
|
|
181
|
+
return (await this.request({
|
|
182
|
+
action: "delete",
|
|
183
|
+
body: opts.data,
|
|
184
|
+
layout: opts.layout,
|
|
185
|
+
timeout: opts.timeout,
|
|
186
|
+
fetchOptions: opts.fetch,
|
|
187
|
+
})) as DeleteResponse;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
layoutMetadata = async (opts: LayoutMetadataOptions): Promise<LayoutMetadataResponse> => {
|
|
191
|
+
return (await this.request({
|
|
192
|
+
action: "metaData",
|
|
193
|
+
layout: opts.layout,
|
|
194
|
+
body: {},
|
|
195
|
+
timeout: opts.timeout,
|
|
196
|
+
fetchOptions: opts.fetch,
|
|
197
|
+
})) as LayoutMetadataResponse;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
executeScript = async (opts: ExecuteScriptOptions): Promise<ScriptResponse> => {
|
|
201
|
+
const res = await fetch(`${this.baseUrl}/callScript`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
connectedFileName: this.connectedFileName,
|
|
206
|
+
scriptName: opts.script,
|
|
207
|
+
data: opts.scriptParam,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
throw new FileMakerError(String(res.status), `FM MCP executeScript failed (${res.status}): ${await res.text()}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const raw = await res.json();
|
|
216
|
+
return {
|
|
217
|
+
scriptResult: typeof raw.result === "string" ? raw.result : JSON.stringify(raw.result),
|
|
218
|
+
} as ScriptResponse;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
containerUpload = (): Promise<never> => {
|
|
222
|
+
throw new Error("Container upload is not supported via FM MCP adapter");
|
|
223
|
+
};
|
|
224
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FetchAdapter } from "./adapters/fetch.js";
|
|
2
|
+
export { FmMcpAdapter, type FmMcpAdapterOptions } from "./adapters/fm-mcp.js";
|
|
2
3
|
export { OttoAdapter, type OttoAPIKey } from "./adapters/otto.js";
|
|
3
4
|
export { DataApi, DataApi as default } from "./client.js";
|
|
4
5
|
export * as clientTypes from "./client-types.js";
|