@ampless/backend 0.2.0-alpha.10 → 0.2.0-alpha.11
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/dist/auth/post-confirmation.js +2 -0
- package/dist/auth/user-admin.js +2 -0
- package/dist/chunk-BYXBJQAS.js +0 -0
- package/dist/events/dispatcher.js +2 -0
- package/dist/events/processor-trusted.js +2 -0
- package/dist/events/processor-untrusted.js +2 -0
- package/dist/functions/api-key-renewer.js +2 -0
- package/dist/functions/mcp-handler.d.ts +16 -8
- package/dist/functions/mcp-handler.js +690 -6
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -2
- package/package.json +7 -2
package/dist/auth/user-admin.js
CHANGED
|
File without changes
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP HTTP endpoint Lambda. Phase
|
|
2
|
+
* MCP HTTP endpoint Lambda. Phase 4: Bearer validation + JSON-RPC 2.0
|
|
3
|
+
* tool dispatch over AppSync IAM auth.
|
|
3
4
|
*
|
|
4
|
-
* Reads
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* 1. Reads KvStore directly (PK = 'mcp-tokens', SK = SHA-256 hex)
|
|
6
|
+
* to validate `Authorization: Bearer amk_...`. Same narrow IAM
|
|
7
|
+
* grant as Phase 3 (`dynamodb:GetItem` on the KvStore table).
|
|
8
|
+
* 2. Parses the incoming JSON-RPC envelope by hand (no MCP SDK
|
|
9
|
+
* stdio transport in a Lambda runtime — overkill for the three
|
|
10
|
+
* verbs we actually need).
|
|
11
|
+
* 3. Dispatches `tools/call` through the shared `@ampless/mcp-server/tools`
|
|
12
|
+
* registry. The GraphqlClient implementation hits AppSync with
|
|
13
|
+
* SigV4 (`AWS_IAM` auth mode), gated by `allow.resource(mcpHandler)
|
|
14
|
+
* .to(['query', 'mutate'])` on Post / PostTag in the schema.
|
|
11
15
|
*
|
|
12
16
|
* Function URL event format: Lambda Function URLs emit API Gateway
|
|
13
17
|
* HTTP v2 events (https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads).
|
|
14
18
|
* Headers arrive lowercased.
|
|
19
|
+
*
|
|
20
|
+
* Note: `upload_media` is filtered out — the StorageClient flow needs
|
|
21
|
+
* presigned S3 PUT URLs (the Lambda doesn't accept the binary body
|
|
22
|
+
* itself), which lands in Phase 5.
|
|
15
23
|
*/
|
|
16
24
|
interface FunctionUrlEvent {
|
|
17
25
|
headers?: Record<string, string | undefined>;
|
|
@@ -1,15 +1,604 @@
|
|
|
1
|
+
import "../chunk-BYXBJQAS.js";
|
|
2
|
+
|
|
1
3
|
// src/functions/mcp-handler.ts
|
|
2
4
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
5
|
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
6
|
import { createHash } from "crypto";
|
|
7
|
+
|
|
8
|
+
// ../mcp-server/dist/chunk-KM6F5QJH.js
|
|
9
|
+
import { composeSiteIdStatus, composeSiteIdSlug } from "ampless";
|
|
10
|
+
import { composeSiteIdStatus as composeSiteIdStatus2, composeSiteIdSlug as composeSiteIdSlug2 } from "ampless";
|
|
11
|
+
var POST_FIELDS = (
|
|
12
|
+
/* GraphQL */
|
|
13
|
+
`
|
|
14
|
+
fragment PostFields on Post {
|
|
15
|
+
siteId
|
|
16
|
+
postId
|
|
17
|
+
slug
|
|
18
|
+
title
|
|
19
|
+
excerpt
|
|
20
|
+
format
|
|
21
|
+
body
|
|
22
|
+
status
|
|
23
|
+
publishedAt
|
|
24
|
+
tags
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
);
|
|
28
|
+
function decodeBody(value) {
|
|
29
|
+
if (typeof value !== "string") return value;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function encodeBody(value) {
|
|
37
|
+
if (typeof value === "string") return value;
|
|
38
|
+
return JSON.stringify(value ?? null);
|
|
39
|
+
}
|
|
40
|
+
function toCorePost(p) {
|
|
41
|
+
return {
|
|
42
|
+
siteId: p.siteId,
|
|
43
|
+
postId: p.postId,
|
|
44
|
+
slug: p.slug,
|
|
45
|
+
title: p.title,
|
|
46
|
+
excerpt: p.excerpt ?? void 0,
|
|
47
|
+
format: p.format ?? "markdown",
|
|
48
|
+
body: decodeBody(p.body),
|
|
49
|
+
status: p.status ?? "draft",
|
|
50
|
+
publishedAt: p.publishedAt ?? void 0,
|
|
51
|
+
tags: (p.tags ?? []).filter((t) => typeof t === "string")
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var QUERY = (
|
|
55
|
+
/* GraphQL */
|
|
56
|
+
`
|
|
57
|
+
${POST_FIELDS}
|
|
58
|
+
query ListPosts($filter: ModelPostFilterInput, $limit: Int, $nextToken: String) {
|
|
59
|
+
listPosts(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
60
|
+
items {
|
|
61
|
+
...PostFields
|
|
62
|
+
}
|
|
63
|
+
nextToken
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
);
|
|
68
|
+
var listPostsSchema = {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
|
|
72
|
+
status: {
|
|
73
|
+
type: "string",
|
|
74
|
+
enum: ["draft", "published", "all"],
|
|
75
|
+
description: 'Filter by status (default "all")'
|
|
76
|
+
},
|
|
77
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "Max results (default 20)" },
|
|
78
|
+
nextToken: { type: "string", description: "Pagination cursor from a previous call" }
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
async function listPosts(client, defaultSiteId, args = {}) {
|
|
82
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
83
|
+
const status = args.status ?? "all";
|
|
84
|
+
const filter = { siteId: { eq: siteId } };
|
|
85
|
+
if (status !== "all") filter.status = { eq: status };
|
|
86
|
+
const data = await client.query(QUERY, { filter, limit: args.limit ?? 20, nextToken: args.nextToken });
|
|
87
|
+
return {
|
|
88
|
+
posts: data.listPosts.items.map(toCorePost),
|
|
89
|
+
nextToken: data.listPosts.nextToken
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var GET_BY_ID = (
|
|
93
|
+
/* GraphQL */
|
|
94
|
+
`
|
|
95
|
+
${POST_FIELDS}
|
|
96
|
+
query GetPost($siteId: String!, $postId: ID!) {
|
|
97
|
+
getPost(siteId: $siteId, postId: $postId) {
|
|
98
|
+
...PostFields
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`
|
|
102
|
+
);
|
|
103
|
+
var LIST_BY_SLUG = (
|
|
104
|
+
/* GraphQL */
|
|
105
|
+
`
|
|
106
|
+
${POST_FIELDS}
|
|
107
|
+
query GetPostBySlug($filter: ModelPostFilterInput!) {
|
|
108
|
+
listPosts(filter: $filter, limit: 1) {
|
|
109
|
+
items {
|
|
110
|
+
...PostFields
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
var getPostSchema = {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
|
|
120
|
+
slug: { type: "string", description: "Post slug" },
|
|
121
|
+
postId: { type: "string", description: "Post id (used when slug is omitted)" }
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
async function getPost(client, defaultSiteId, args) {
|
|
125
|
+
if (!args.slug && !args.postId) {
|
|
126
|
+
throw new Error("get_post requires either `slug` or `postId`");
|
|
127
|
+
}
|
|
128
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
129
|
+
if (args.postId) {
|
|
130
|
+
const data2 = await client.query(GET_BY_ID, { siteId, postId: args.postId });
|
|
131
|
+
return data2.getPost ? toCorePost(data2.getPost) : null;
|
|
132
|
+
}
|
|
133
|
+
const data = await client.query(LIST_BY_SLUG, {
|
|
134
|
+
filter: { siteId: { eq: siteId }, slug: { eq: args.slug } }
|
|
135
|
+
});
|
|
136
|
+
const item = data.listPosts.items[0];
|
|
137
|
+
return item ? toCorePost(item) : null;
|
|
138
|
+
}
|
|
139
|
+
function entries(post) {
|
|
140
|
+
if (post.status !== "published" || !post.publishedAt || !post.tags?.length) return [];
|
|
141
|
+
return post.tags.map((tag) => ({
|
|
142
|
+
siteIdTag: `${post.siteId}#${tag}`,
|
|
143
|
+
publishedAtPostId: `${post.publishedAt}#${post.postId}`
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function entryKey(e) {
|
|
147
|
+
return `${e.siteIdTag}|${e.publishedAtPostId}`;
|
|
148
|
+
}
|
|
149
|
+
var CREATE_POST_TAG = (
|
|
150
|
+
/* GraphQL */
|
|
151
|
+
`
|
|
152
|
+
mutation CreatePostTag($input: CreatePostTagInput!) {
|
|
153
|
+
createPostTag(input: $input) {
|
|
154
|
+
siteIdTag
|
|
155
|
+
publishedAtPostId
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`
|
|
159
|
+
);
|
|
160
|
+
var UPDATE_POST_TAG = (
|
|
161
|
+
/* GraphQL */
|
|
162
|
+
`
|
|
163
|
+
mutation UpdatePostTag($input: UpdatePostTagInput!) {
|
|
164
|
+
updatePostTag(input: $input) {
|
|
165
|
+
siteIdTag
|
|
166
|
+
publishedAtPostId
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`
|
|
170
|
+
);
|
|
171
|
+
var DELETE_POST_TAG = (
|
|
172
|
+
/* GraphQL */
|
|
173
|
+
`
|
|
174
|
+
mutation DeletePostTag($input: DeletePostTagInput!) {
|
|
175
|
+
deletePostTag(input: $input) {
|
|
176
|
+
siteIdTag
|
|
177
|
+
publishedAtPostId
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
`
|
|
181
|
+
);
|
|
182
|
+
async function syncPostTags(client, post, oldPost) {
|
|
183
|
+
const oldEntries = oldPost ? entries(oldPost) : [];
|
|
184
|
+
const newEntries = entries(post);
|
|
185
|
+
const oldKeys = new Set(oldEntries.map(entryKey));
|
|
186
|
+
const newKeys = new Set(newEntries.map(entryKey));
|
|
187
|
+
await Promise.all(
|
|
188
|
+
oldEntries.filter((e) => !newKeys.has(entryKey(e))).map(
|
|
189
|
+
(e) => client.query(DELETE_POST_TAG, {
|
|
190
|
+
input: { siteIdTag: e.siteIdTag, publishedAtPostId: e.publishedAtPostId }
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
await Promise.all(
|
|
195
|
+
newEntries.filter((e) => !oldKeys.has(entryKey(e))).map(
|
|
196
|
+
(e) => client.query(CREATE_POST_TAG, {
|
|
197
|
+
input: {
|
|
198
|
+
siteIdTag: e.siteIdTag,
|
|
199
|
+
publishedAtPostId: e.publishedAtPostId,
|
|
200
|
+
siteId: post.siteId,
|
|
201
|
+
tag: e.siteIdTag.slice(post.siteId.length + 1),
|
|
202
|
+
postId: post.postId,
|
|
203
|
+
publishedAt: post.publishedAt,
|
|
204
|
+
slug: post.slug,
|
|
205
|
+
title: post.title,
|
|
206
|
+
excerpt: post.excerpt,
|
|
207
|
+
tags: post.tags ?? []
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
await Promise.all(
|
|
213
|
+
newEntries.filter((e) => oldKeys.has(entryKey(e))).map(
|
|
214
|
+
(e) => client.query(UPDATE_POST_TAG, {
|
|
215
|
+
input: {
|
|
216
|
+
siteIdTag: e.siteIdTag,
|
|
217
|
+
publishedAtPostId: e.publishedAtPostId,
|
|
218
|
+
slug: post.slug,
|
|
219
|
+
title: post.title,
|
|
220
|
+
excerpt: post.excerpt,
|
|
221
|
+
tags: post.tags ?? []
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
var MUTATION = (
|
|
228
|
+
/* GraphQL */
|
|
229
|
+
`
|
|
230
|
+
${POST_FIELDS}
|
|
231
|
+
mutation CreatePost($input: CreatePostInput!) {
|
|
232
|
+
createPost(input: $input) {
|
|
233
|
+
...PostFields
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
`
|
|
237
|
+
);
|
|
238
|
+
var createPostSchema = {
|
|
239
|
+
type: "object",
|
|
240
|
+
required: ["slug", "title", "format", "body"],
|
|
241
|
+
properties: {
|
|
242
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
|
|
243
|
+
postId: { type: "string", description: "Optional explicit id; auto-generated if omitted" },
|
|
244
|
+
slug: { type: "string" },
|
|
245
|
+
title: { type: "string" },
|
|
246
|
+
format: { type: "string", enum: ["tiptap", "markdown", "html"] },
|
|
247
|
+
body: {
|
|
248
|
+
description: "tiptap JSON (when format=tiptap), markdown source string, or raw HTML string"
|
|
249
|
+
},
|
|
250
|
+
status: { type: "string", enum: ["draft", "published"], default: "draft" },
|
|
251
|
+
excerpt: { type: "string" },
|
|
252
|
+
publishedAt: { type: "string", description: "ISO 8601 timestamp; required when status=published" },
|
|
253
|
+
tags: { type: "array", items: { type: "string" } }
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
async function createPost(client, defaultSiteId, args) {
|
|
257
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
258
|
+
const postId = args.postId ?? `post-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
259
|
+
const status = args.status ?? "draft";
|
|
260
|
+
const publishedAt = args.publishedAt ?? (status === "published" ? (/* @__PURE__ */ new Date()).toISOString() : void 0);
|
|
261
|
+
const data = await client.query(MUTATION, {
|
|
262
|
+
input: {
|
|
263
|
+
siteId,
|
|
264
|
+
postId,
|
|
265
|
+
slug: args.slug,
|
|
266
|
+
title: args.title,
|
|
267
|
+
excerpt: args.excerpt,
|
|
268
|
+
format: args.format,
|
|
269
|
+
body: encodeBody(args.body),
|
|
270
|
+
status,
|
|
271
|
+
publishedAt,
|
|
272
|
+
tags: args.tags,
|
|
273
|
+
// Denormalized GSI keys.
|
|
274
|
+
siteIdStatus: composeSiteIdStatus(siteId, status),
|
|
275
|
+
siteIdSlug: composeSiteIdSlug(siteId, args.slug)
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
const created = toCorePost(data.createPost);
|
|
279
|
+
await syncPostTags(client, created, null);
|
|
280
|
+
return created;
|
|
281
|
+
}
|
|
282
|
+
var MUTATION2 = (
|
|
283
|
+
/* GraphQL */
|
|
284
|
+
`
|
|
285
|
+
${POST_FIELDS}
|
|
286
|
+
mutation UpdatePost($input: UpdatePostInput!) {
|
|
287
|
+
updatePost(input: $input) {
|
|
288
|
+
...PostFields
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`
|
|
292
|
+
);
|
|
293
|
+
var updatePostSchema = {
|
|
294
|
+
type: "object",
|
|
295
|
+
required: ["postId"],
|
|
296
|
+
properties: {
|
|
297
|
+
postId: { type: "string" },
|
|
298
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
|
|
299
|
+
slug: { type: "string" },
|
|
300
|
+
title: { type: "string" },
|
|
301
|
+
excerpt: { type: "string" },
|
|
302
|
+
format: { type: "string", enum: ["tiptap", "markdown", "html"] },
|
|
303
|
+
body: { description: "tiptap JSON, markdown source, or raw HTML" },
|
|
304
|
+
status: { type: "string", enum: ["draft", "published"] },
|
|
305
|
+
publishedAt: { type: "string", description: "ISO 8601 timestamp" },
|
|
306
|
+
tags: { type: "array", items: { type: "string" } }
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
async function updatePost(client, defaultSiteId, args) {
|
|
310
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
311
|
+
const oldPost = await getPost(client, defaultSiteId, { postId: args.postId, siteId });
|
|
312
|
+
const input = { siteId, postId: args.postId };
|
|
313
|
+
if (args.slug !== void 0) input.slug = args.slug;
|
|
314
|
+
if (args.title !== void 0) input.title = args.title;
|
|
315
|
+
if (args.excerpt !== void 0) input.excerpt = args.excerpt;
|
|
316
|
+
if (args.format !== void 0) input.format = args.format;
|
|
317
|
+
if (args.body !== void 0) input.body = encodeBody(args.body);
|
|
318
|
+
if (args.status !== void 0) input.status = args.status;
|
|
319
|
+
if (args.publishedAt !== void 0) input.publishedAt = args.publishedAt;
|
|
320
|
+
if (args.tags !== void 0) input.tags = args.tags;
|
|
321
|
+
if (args.status !== void 0) {
|
|
322
|
+
input.siteIdStatus = composeSiteIdStatus2(siteId, args.status);
|
|
323
|
+
}
|
|
324
|
+
if (args.slug !== void 0) {
|
|
325
|
+
input.siteIdSlug = composeSiteIdSlug2(siteId, args.slug);
|
|
326
|
+
}
|
|
327
|
+
const data = await client.query(MUTATION2, { input });
|
|
328
|
+
const updated = toCorePost(data.updatePost);
|
|
329
|
+
await syncPostTags(client, updated, oldPost);
|
|
330
|
+
return updated;
|
|
331
|
+
}
|
|
332
|
+
var MUTATION3 = (
|
|
333
|
+
/* GraphQL */
|
|
334
|
+
`
|
|
335
|
+
mutation DeletePost($input: DeletePostInput!) {
|
|
336
|
+
deletePost(input: $input) {
|
|
337
|
+
siteId
|
|
338
|
+
postId
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
`
|
|
342
|
+
);
|
|
343
|
+
var deletePostSchema = {
|
|
344
|
+
type: "object",
|
|
345
|
+
required: ["postId"],
|
|
346
|
+
properties: {
|
|
347
|
+
postId: { type: "string" },
|
|
348
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' }
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
async function deletePost(client, defaultSiteId, args) {
|
|
352
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
353
|
+
const oldPost = await getPost(client, defaultSiteId, { postId: args.postId, siteId });
|
|
354
|
+
if (oldPost) {
|
|
355
|
+
await syncPostTags(client, { ...oldPost, status: "draft" }, oldPost);
|
|
356
|
+
}
|
|
357
|
+
const data = await client.query(MUTATION3, { input: { siteId, postId: args.postId } });
|
|
358
|
+
return { deleted: data.deletePost };
|
|
359
|
+
}
|
|
360
|
+
function sanitizeName(name) {
|
|
361
|
+
return name.replace(/[\x00-\x1f\x7f]/g, "").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, "_").replace(/^\.+/, "_").slice(0, 200) || "upload";
|
|
362
|
+
}
|
|
363
|
+
function buildMediaKey(filename, now = /* @__PURE__ */ new Date()) {
|
|
364
|
+
const safe = sanitizeName(filename);
|
|
365
|
+
const yyyy = now.getFullYear();
|
|
366
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
367
|
+
return `public/media/${yyyy}/${mm}/${now.getTime()}-${safe}`;
|
|
368
|
+
}
|
|
369
|
+
var MUTATION4 = (
|
|
370
|
+
/* GraphQL */
|
|
371
|
+
`
|
|
372
|
+
mutation CreateMedia($input: CreateMediaInput!) {
|
|
373
|
+
createMedia(input: $input) {
|
|
374
|
+
siteId
|
|
375
|
+
mediaId
|
|
376
|
+
src
|
|
377
|
+
mimeType
|
|
378
|
+
size
|
|
379
|
+
delivery
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
`
|
|
383
|
+
);
|
|
384
|
+
var uploadMediaSchema = {
|
|
385
|
+
type: "object",
|
|
386
|
+
required: ["filename", "mimeType", "base64Data"],
|
|
387
|
+
properties: {
|
|
388
|
+
siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
|
|
389
|
+
filename: { type: "string", description: "Original filename; sanitized server-side" },
|
|
390
|
+
mimeType: {
|
|
391
|
+
type: "string",
|
|
392
|
+
description: "IANA media type (e.g. image/webp, image/jpeg). The MCP server does not transcode \u2014 pass the MIME of the bytes you actually send."
|
|
393
|
+
},
|
|
394
|
+
base64Data: {
|
|
395
|
+
type: "string",
|
|
396
|
+
description: "Base64-encoded file contents. NO data:URL prefix. The MCP server uploads bytes verbatim \u2014 resize/encode client-side first if needed."
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
async function uploadMedia(graphql, storage, defaultSiteId, args) {
|
|
401
|
+
const siteId = args.siteId ?? defaultSiteId;
|
|
402
|
+
const body = Buffer.from(args.base64Data, "base64");
|
|
403
|
+
const key = buildMediaKey(args.filename);
|
|
404
|
+
const url = await storage.putObject(key, body, args.mimeType);
|
|
405
|
+
const mediaId = `media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
406
|
+
const data = await graphql.query(MUTATION4, {
|
|
407
|
+
input: {
|
|
408
|
+
siteId,
|
|
409
|
+
mediaId,
|
|
410
|
+
src: key,
|
|
411
|
+
mimeType: args.mimeType,
|
|
412
|
+
size: body.length,
|
|
413
|
+
delivery: "nextjs"
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return { media: data.createMedia, url };
|
|
417
|
+
}
|
|
418
|
+
var getSchemaSchema = {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {}
|
|
421
|
+
};
|
|
422
|
+
function getSchema() {
|
|
423
|
+
return {
|
|
424
|
+
contentTypes: [
|
|
425
|
+
{
|
|
426
|
+
name: "post",
|
|
427
|
+
identifier: ["siteId", "postId"],
|
|
428
|
+
fields: {
|
|
429
|
+
siteId: { type: "string", required: true },
|
|
430
|
+
postId: { type: "string", required: true, description: "auto-generated if omitted on create" },
|
|
431
|
+
slug: { type: "string", required: true, description: "URL slug, unique per site" },
|
|
432
|
+
title: { type: "string", required: true },
|
|
433
|
+
excerpt: { type: "string" },
|
|
434
|
+
format: { type: "enum", values: ["tiptap", "markdown", "html"], required: true },
|
|
435
|
+
body: {
|
|
436
|
+
type: "json",
|
|
437
|
+
description: "tiptap node JSON when format=tiptap; markdown source string when format=markdown; raw HTML string when format=html"
|
|
438
|
+
},
|
|
439
|
+
status: { type: "enum", values: ["draft", "published"], default: "draft" },
|
|
440
|
+
publishedAt: { type: "datetime", description: "ISO 8601" },
|
|
441
|
+
tags: { type: "string[]" }
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "page",
|
|
446
|
+
identifier: ["siteId", "pageId"],
|
|
447
|
+
fields: {
|
|
448
|
+
siteId: { type: "string", required: true },
|
|
449
|
+
pageId: { type: "string", required: true },
|
|
450
|
+
slug: { type: "string", required: true },
|
|
451
|
+
title: { type: "string", required: true },
|
|
452
|
+
format: { type: "enum", values: ["tiptap", "markdown", "html"], required: true },
|
|
453
|
+
body: { type: "json" },
|
|
454
|
+
status: { type: "enum", values: ["draft", "published"] },
|
|
455
|
+
publishedAt: { type: "datetime" }
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "media",
|
|
460
|
+
identifier: ["siteId", "mediaId"],
|
|
461
|
+
fields: {
|
|
462
|
+
siteId: { type: "string", required: true },
|
|
463
|
+
mediaId: { type: "string", required: true },
|
|
464
|
+
src: { type: "string", required: true, description: "S3 key" },
|
|
465
|
+
mimeType: { type: "string", required: true },
|
|
466
|
+
size: { type: "integer" },
|
|
467
|
+
delivery: { type: "string" }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
],
|
|
471
|
+
formats: ["tiptap", "markdown", "html"],
|
|
472
|
+
notes: {
|
|
473
|
+
editorTrust: 'editor stores arbitrary HTML/JS verbatim \u2014 same trust shape as WordPress unfiltered_html capability. See docs/architecture/04-access-layer-mcp.md \xA7"editor \u306E\u4FE1\u983C\u30E2\u30C7\u30EB".',
|
|
474
|
+
tiptapBody: 'When format=tiptap, body is the tiptap document JSON: { type: "doc", content: [...] }. The renderer expects this shape.'
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
var tools = [
|
|
479
|
+
{
|
|
480
|
+
name: "list_posts",
|
|
481
|
+
description: "List posts in the CMS with optional filters by status. Returns up to `limit` posts (default 20) plus a `nextToken` cursor for pagination.",
|
|
482
|
+
inputSchema: listPostsSchema,
|
|
483
|
+
handler: (args, ctx) => listPosts(ctx.graphql, ctx.defaultSiteId, args)
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "get_post",
|
|
487
|
+
description: "Fetch a single post by slug or postId. Returns null if not found.",
|
|
488
|
+
inputSchema: getPostSchema,
|
|
489
|
+
handler: (args, ctx) => getPost(ctx.graphql, ctx.defaultSiteId, args)
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "create_post",
|
|
493
|
+
description: "Create a new post. Title and slug are required. Body shape depends on format: tiptap=JSON node tree, markdown=source string, html=raw HTML string. Defaults to status=draft.",
|
|
494
|
+
inputSchema: createPostSchema,
|
|
495
|
+
handler: (args, ctx) => createPost(ctx.graphql, ctx.defaultSiteId, args)
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "update_post",
|
|
499
|
+
description: "Update an existing post by postId. Only the fields you pass are changed. Tag list / publishedAt changes also update the PostTag denormalized index.",
|
|
500
|
+
inputSchema: updatePostSchema,
|
|
501
|
+
handler: (args, ctx) => updatePost(ctx.graphql, ctx.defaultSiteId, args)
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "delete_post",
|
|
505
|
+
description: "Delete a post by postId. Also drops associated PostTag index entries.",
|
|
506
|
+
inputSchema: deletePostSchema,
|
|
507
|
+
handler: (args, ctx) => deletePost(ctx.graphql, ctx.defaultSiteId, args)
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "upload_media",
|
|
511
|
+
description: "Upload a file to the site's media S3 bucket. Pass base64-encoded bytes; the server stores them verbatim under public/media/YYYY/MM/. Returns the public URL and Media record. The server does not transcode \u2014 pre-process (e.g. resize/webp) on the client.",
|
|
512
|
+
inputSchema: uploadMediaSchema,
|
|
513
|
+
handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), ctx.defaultSiteId, args)
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "get_schema",
|
|
517
|
+
description: "Returns the CMS content schema (Post/Page/Media field shapes, format enum, notes). Useful as the first call to understand what fields are available.",
|
|
518
|
+
inputSchema: getSchemaSchema,
|
|
519
|
+
handler: async () => getSchema()
|
|
520
|
+
}
|
|
521
|
+
];
|
|
522
|
+
async function dispatchToolCall(name, args, ctx) {
|
|
523
|
+
const tool = tools.find((t) => t.name === name);
|
|
524
|
+
if (!tool) return null;
|
|
525
|
+
return tool.handler(args, ctx);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/functions/mcp-graphql-client.ts
|
|
529
|
+
import { Sha256 } from "@aws-crypto/sha256-js";
|
|
530
|
+
import { HttpRequest } from "@smithy/protocol-http";
|
|
531
|
+
import { SignatureV4 } from "@smithy/signature-v4";
|
|
532
|
+
import { defaultProvider } from "@aws-sdk/credential-provider-node";
|
|
533
|
+
function createMcpGraphqlClient(opts) {
|
|
534
|
+
const url = new URL(opts.endpoint);
|
|
535
|
+
const signer = new SignatureV4({
|
|
536
|
+
service: "appsync",
|
|
537
|
+
region: opts.region,
|
|
538
|
+
credentials: defaultProvider(),
|
|
539
|
+
sha256: Sha256
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
async query(operation, variables = {}) {
|
|
543
|
+
const body = JSON.stringify({ query: operation, variables });
|
|
544
|
+
const request = new HttpRequest({
|
|
545
|
+
method: "POST",
|
|
546
|
+
protocol: url.protocol,
|
|
547
|
+
hostname: url.hostname,
|
|
548
|
+
path: url.pathname,
|
|
549
|
+
headers: {
|
|
550
|
+
// `host` is required for SigV4 canonicalisation.
|
|
551
|
+
host: url.hostname,
|
|
552
|
+
"content-type": "application/json"
|
|
553
|
+
},
|
|
554
|
+
body
|
|
555
|
+
});
|
|
556
|
+
const signed = await signer.sign(request);
|
|
557
|
+
const response = await fetch(opts.endpoint, {
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers: signed.headers,
|
|
560
|
+
body
|
|
561
|
+
});
|
|
562
|
+
if (!response.ok) {
|
|
563
|
+
const text = await response.text().catch(() => "");
|
|
564
|
+
console.error("[mcp-graphql-client] AppSync HTTP error", {
|
|
565
|
+
status: response.status,
|
|
566
|
+
body: text.slice(0, 1e3)
|
|
567
|
+
});
|
|
568
|
+
throw new Error(`AppSync ${response.status}: ${text || response.statusText}`);
|
|
569
|
+
}
|
|
570
|
+
const json = await response.json();
|
|
571
|
+
if (json.errors && json.errors.length > 0) {
|
|
572
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
573
|
+
console.error("[mcp-graphql-client] AppSync GraphQL errors", { errors: json.errors });
|
|
574
|
+
throw new Error(`AppSync GraphQL error: ${msg}`);
|
|
575
|
+
}
|
|
576
|
+
if (!json.data) {
|
|
577
|
+
throw new Error("AppSync returned an empty response");
|
|
578
|
+
}
|
|
579
|
+
return json.data;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/functions/mcp-handler.ts
|
|
5
585
|
var TOKENS_PK = "mcp-tokens";
|
|
586
|
+
var JSON_RPC_PARSE_ERROR = -32700;
|
|
587
|
+
var JSON_RPC_INVALID_REQUEST = -32600;
|
|
588
|
+
var JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
589
|
+
var JSON_RPC_INVALID_PARAMS = -32602;
|
|
590
|
+
var JSON_RPC_INTERNAL_ERROR = -32603;
|
|
591
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
6
592
|
function requireEnv(name) {
|
|
7
593
|
const v = process.env[name];
|
|
8
594
|
if (!v) throw new Error(`[mcp-handler] missing required env var ${name}`);
|
|
9
595
|
return v;
|
|
10
596
|
}
|
|
11
597
|
var KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
|
|
598
|
+
var APPSYNC_URL = requireEnv("AMPLESS_APPSYNC_URL");
|
|
599
|
+
var AWS_REGION = process.env["AWS_REGION"] ?? "us-east-1";
|
|
12
600
|
var ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
601
|
+
var HTTP_TOOLS = tools.filter((t) => t.name !== "upload_media");
|
|
13
602
|
function hashToken(plain) {
|
|
14
603
|
return createHash("sha256").update(plain).digest("hex");
|
|
15
604
|
}
|
|
@@ -20,6 +609,14 @@ function jsonResponse(statusCode, body) {
|
|
|
20
609
|
body: JSON.stringify(body)
|
|
21
610
|
};
|
|
22
611
|
}
|
|
612
|
+
function jsonRpcResult(id, result) {
|
|
613
|
+
return { jsonrpc: "2.0", id, result };
|
|
614
|
+
}
|
|
615
|
+
function jsonRpcError(id, code, message, data) {
|
|
616
|
+
const error = { code, message };
|
|
617
|
+
if (data !== void 0) error.data = data;
|
|
618
|
+
return { jsonrpc: "2.0", id, error };
|
|
619
|
+
}
|
|
23
620
|
async function validateBearer(plaintext) {
|
|
24
621
|
const hash = hashToken(plaintext);
|
|
25
622
|
const res = await ddb.send(
|
|
@@ -41,6 +638,72 @@ async function validateBearer(plaintext) {
|
|
|
41
638
|
if (meta.expiresAt && new Date(meta.expiresAt).getTime() <= Date.now()) return null;
|
|
42
639
|
return meta;
|
|
43
640
|
}
|
|
641
|
+
var cachedCtx = null;
|
|
642
|
+
function makeContext(meta) {
|
|
643
|
+
if (cachedCtx && cachedCtx.defaultSiteId === (meta.scope.siteId ?? "default")) {
|
|
644
|
+
return cachedCtx;
|
|
645
|
+
}
|
|
646
|
+
const ctx = {
|
|
647
|
+
graphql: createMcpGraphqlClient({ endpoint: APPSYNC_URL, region: AWS_REGION }),
|
|
648
|
+
storage: () => {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"upload_media is not available on the HTTP MCP transport in v0.2 Phase 4 \u2014 use the stdio CLI or wait for Phase 5"
|
|
651
|
+
);
|
|
652
|
+
},
|
|
653
|
+
defaultSiteId: meta.scope.siteId ?? "default"
|
|
654
|
+
};
|
|
655
|
+
cachedCtx = ctx;
|
|
656
|
+
return ctx;
|
|
657
|
+
}
|
|
658
|
+
async function dispatchJsonRpc(req, meta) {
|
|
659
|
+
switch (req.method) {
|
|
660
|
+
case "initialize":
|
|
661
|
+
return jsonRpcResult(req.id, {
|
|
662
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
663
|
+
capabilities: { tools: {} },
|
|
664
|
+
serverInfo: { name: "ampless-mcp", version: "0.2" }
|
|
665
|
+
});
|
|
666
|
+
case "notifications/initialized":
|
|
667
|
+
return jsonRpcResult(req.id, null);
|
|
668
|
+
case "tools/list":
|
|
669
|
+
return jsonRpcResult(req.id, {
|
|
670
|
+
tools: HTTP_TOOLS.map((t) => ({
|
|
671
|
+
name: t.name,
|
|
672
|
+
description: t.description,
|
|
673
|
+
inputSchema: t.inputSchema
|
|
674
|
+
}))
|
|
675
|
+
});
|
|
676
|
+
case "tools/call": {
|
|
677
|
+
const params = req.params;
|
|
678
|
+
if (!params?.name || typeof params.name !== "string") {
|
|
679
|
+
return jsonRpcError(req.id, JSON_RPC_INVALID_PARAMS, "tools/call requires a `name` parameter");
|
|
680
|
+
}
|
|
681
|
+
const tool = HTTP_TOOLS.find((t) => t.name === params.name);
|
|
682
|
+
if (!tool) {
|
|
683
|
+
return jsonRpcError(req.id, JSON_RPC_METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
|
|
684
|
+
}
|
|
685
|
+
const ctx = makeContext(meta);
|
|
686
|
+
try {
|
|
687
|
+
const result = await dispatchToolCall(params.name, params.arguments ?? {}, ctx);
|
|
688
|
+
return jsonRpcResult(req.id, {
|
|
689
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
690
|
+
});
|
|
691
|
+
} catch (err) {
|
|
692
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
693
|
+
console.error("[mcp-handler] tool dispatch failed", {
|
|
694
|
+
tool: params.name,
|
|
695
|
+
message
|
|
696
|
+
});
|
|
697
|
+
return jsonRpcResult(req.id, {
|
|
698
|
+
isError: true,
|
|
699
|
+
content: [{ type: "text", text: message }]
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
default:
|
|
704
|
+
return jsonRpcError(req.id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${req.method}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
44
707
|
var handler = async (event) => {
|
|
45
708
|
if (event.requestContext?.http?.method === "OPTIONS") {
|
|
46
709
|
return { statusCode: 204, body: "" };
|
|
@@ -58,12 +721,33 @@ var handler = async (event) => {
|
|
|
58
721
|
if (!meta) {
|
|
59
722
|
return jsonResponse(401, { error: "invalid_token" });
|
|
60
723
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
724
|
+
let req;
|
|
725
|
+
try {
|
|
726
|
+
const body = event.isBase64Encoded ? Buffer.from(event.body ?? "", "base64").toString("utf8") : event.body ?? "";
|
|
727
|
+
if (!body) {
|
|
728
|
+
return jsonResponse(400, jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Empty body"));
|
|
729
|
+
}
|
|
730
|
+
req = JSON.parse(body);
|
|
731
|
+
} catch {
|
|
732
|
+
return jsonResponse(400, jsonRpcError(null, JSON_RPC_PARSE_ERROR, "Parse error"));
|
|
733
|
+
}
|
|
734
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
|
|
735
|
+
return jsonResponse(
|
|
736
|
+
400,
|
|
737
|
+
jsonRpcError(req?.id ?? null, JSON_RPC_INVALID_REQUEST, "Invalid Request")
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const response = await dispatchJsonRpc(req, meta);
|
|
742
|
+
return jsonResponse(200, response);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
745
|
+
console.error("[mcp-handler] dispatch threw", { method: req.method, message });
|
|
746
|
+
return jsonResponse(
|
|
747
|
+
500,
|
|
748
|
+
jsonRpcError(req.id ?? null, JSON_RPC_INTERNAL_ERROR, message)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
67
751
|
};
|
|
68
752
|
export {
|
|
69
753
|
handler
|
package/dist/index.d.ts
CHANGED
|
@@ -149,6 +149,22 @@ interface AmplessSchemaModelsOpts {
|
|
|
149
149
|
* pattern as `AmplessAuthConfigOpts.postConfirmation`.
|
|
150
150
|
*/
|
|
151
151
|
userAdminFunction?: unknown;
|
|
152
|
+
/**
|
|
153
|
+
* Optional Amplify `defineFunction` ref backing the MCP HTTP Lambda.
|
|
154
|
+
* When supplied, the Post and PostTag models gain an `allow.resource(...)`
|
|
155
|
+
* authorization clause granting the Lambda `query` + `mutate` access
|
|
156
|
+
* via AppSync IAM auth. The existing group-based clauses stay intact —
|
|
157
|
+
* this just adds a second principal so the Lambda can dispatch tool
|
|
158
|
+
* calls under its own scoped role, with no Cognito identity or
|
|
159
|
+
* shared API key involved.
|
|
160
|
+
*
|
|
161
|
+
* Phase 4 covers Post + PostTag only (the read/write surface for the
|
|
162
|
+
* post CRUD tools). Media gets the same treatment in Phase 5 once
|
|
163
|
+
* the presigned-PUT upload flow lands.
|
|
164
|
+
*
|
|
165
|
+
* Typed as `unknown` for the same reason as `userAdminFunction`.
|
|
166
|
+
*/
|
|
167
|
+
mcpHandlerFunction?: unknown;
|
|
152
168
|
}
|
|
153
169
|
/**
|
|
154
170
|
* The Ampless model + custom query definitions, returned as a plain
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./chunk-BYXBJQAS.js";
|
|
2
|
+
|
|
1
3
|
// src/backend.ts
|
|
2
4
|
import { defineBackend } from "@aws-amplify/backend";
|
|
3
5
|
import { Effect, PolicyStatement, AnyPrincipal } from "aws-cdk-lib/aws-iam";
|
|
@@ -176,6 +178,10 @@ function defineAmplessBackend(opts) {
|
|
|
176
178
|
})
|
|
177
179
|
);
|
|
178
180
|
mcpHandlerFn.addEnvironment("AMPLESS_KV_TABLE", kvTable.tableName);
|
|
181
|
+
mcpHandlerFn.addEnvironment(
|
|
182
|
+
"AMPLESS_APPSYNC_URL",
|
|
183
|
+
backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl
|
|
184
|
+
);
|
|
179
185
|
const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
|
|
180
186
|
authType: FunctionUrlAuthType.NONE,
|
|
181
187
|
cors: {
|
|
@@ -271,7 +277,8 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
271
277
|
index("siteIdStatus").sortKeys(["publishedAt"]).name("bySiteIdStatus"),
|
|
272
278
|
index("siteIdSlug").name("bySiteIdSlug")
|
|
273
279
|
]).authorization((allow) => [
|
|
274
|
-
allow.groups(["ampless-admin", "ampless-editor"])
|
|
280
|
+
allow.groups(["ampless-admin", "ampless-editor"]),
|
|
281
|
+
...opts.mcpHandlerFunction ? [allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"])] : []
|
|
275
282
|
]),
|
|
276
283
|
Page: a.model({
|
|
277
284
|
siteId: a.string().required(),
|
|
@@ -325,7 +332,8 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
325
332
|
// Full tag list of the post (for chip rendering on tag pages).
|
|
326
333
|
tags: a.string().array()
|
|
327
334
|
}).identifier(["siteIdTag", "publishedAtPostId"]).authorization((allow) => [
|
|
328
|
-
allow.groups(["ampless-admin", "ampless-editor"])
|
|
335
|
+
allow.groups(["ampless-admin", "ampless-editor"]),
|
|
336
|
+
...opts.mcpHandlerFunction ? [allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"])] : []
|
|
329
337
|
]),
|
|
330
338
|
// Generic key/value store. Two roles in one table:
|
|
331
339
|
// - Site settings: PK = `siteconfig:{siteId}`, SK = dotted key
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.11",
|
|
4
4
|
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -53,14 +53,19 @@
|
|
|
53
53
|
"homepage": "https://github.com/heavymoons/ampless/tree/main/packages/backend#readme",
|
|
54
54
|
"bugs": "https://github.com/heavymoons/ampless/issues",
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@aws-crypto/sha256-js": "^5.2.0",
|
|
56
57
|
"@aws-sdk/client-appsync": "^3.717.0",
|
|
57
58
|
"@aws-sdk/client-cognito-identity-provider": "^3.717.0",
|
|
58
59
|
"@aws-sdk/client-dynamodb": "^3.717.0",
|
|
59
60
|
"@aws-sdk/client-s3": "^3.1048.0",
|
|
60
61
|
"@aws-sdk/client-sqs": "^3.717.0",
|
|
62
|
+
"@aws-sdk/credential-provider-node": "^3.717.0",
|
|
61
63
|
"@aws-sdk/lib-dynamodb": "^3.717.0",
|
|
62
64
|
"@aws-sdk/util-dynamodb": "^3.717.0",
|
|
63
|
-
"
|
|
65
|
+
"@smithy/protocol-http": "^5.3.0",
|
|
66
|
+
"@smithy/signature-v4": "^5.4.0",
|
|
67
|
+
"ampless": "0.2.0-alpha.6",
|
|
68
|
+
"@ampless/mcp-server": "0.2.0-alpha.6"
|
|
64
69
|
},
|
|
65
70
|
"peerDependencies": {
|
|
66
71
|
"@aws-amplify/backend": "^1",
|