@ampless/mcp-server 1.0.0-alpha.24 → 1.0.0-alpha.26
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.ja.md +3 -0
- package/README.md +3 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +293 -0
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -68,6 +68,9 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
|
|
|
68
68
|
| `update_post` | editor | 既存の投稿のフィールドをパッチ更新 |
|
|
69
69
|
| `delete_post` | editor | 投稿を削除しタグインデックスをクリーンアップ |
|
|
70
70
|
| `upload_media` | editor | Base64 エンコードされたバイト列を S3 にアップロードし Media レコードを作成 |
|
|
71
|
+
| `list_media` | reader | メディア一覧を取得。`mimeType`(前方一致)/ `prefix` / `createdAfter` / `createdBefore` フィルターとページネーション対応 |
|
|
72
|
+
| `search_media` | reader | ファイル名 / `src` / `mimeType` への部分一致でメディアを検索 |
|
|
73
|
+
| `delete_media` | editor | メディアファイルを削除(S3 オブジェクト + Media 行)。`mediaId` または `src` で指定。`dryRun: true` で削除せずにプレビュー |
|
|
71
74
|
| `get_schema` | reader | CMS コンテンツスキーマを返す |
|
|
72
75
|
| `upload_static_bundle` | editor | ビルド済み静的バンドルを S3 にアップロード |
|
|
73
76
|
| `list_static_files` | reader | 静的バンドルファイルを一覧表示 |
|
package/README.md
CHANGED
|
@@ -68,6 +68,9 @@ import type { ToolDefinition, ToolContext, ResolvedSite } from '@ampless/mcp-ser
|
|
|
68
68
|
| `update_post` | editor | Patch fields on an existing post |
|
|
69
69
|
| `delete_post` | editor | Delete a post and clean up its tag index |
|
|
70
70
|
| `upload_media` | editor | Upload base64-encoded bytes to S3 and create a Media record |
|
|
71
|
+
| `list_media` | reader | List media with optional `mimeType` (prefix) / `prefix` / `createdAfter` / `createdBefore` filters + pagination |
|
|
72
|
+
| `search_media` | reader | Search media by substring across filename / `src` / `mimeType` |
|
|
73
|
+
| `delete_media` | editor | Delete a media file (S3 object + Media row). Pass `mediaId` or `src`; `dryRun: true` previews without deleting |
|
|
71
74
|
| `get_schema` | reader | Return the CMS content schema |
|
|
72
75
|
| `upload_static_bundle` | editor | Upload a pre-built static bundle to S3 |
|
|
73
76
|
| `list_static_files` | reader | List static bundle files |
|
package/dist/index.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ interface StorageClient {
|
|
|
68
68
|
* a few hundred entries per bundle in practice.
|
|
69
69
|
*/
|
|
70
70
|
listObjects(prefix: string): Promise<StorageObject[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Build the public URL for an object `key`, in the same format
|
|
73
|
+
* `putObject` returns (`https://{bucket}.s3.{region}.amazonaws.com/{key}`).
|
|
74
|
+
* Pure string formatting — no S3 round-trip. `list_media` /
|
|
75
|
+
* `search_media` use this to surface a `url` alongside each Media row
|
|
76
|
+
* without re-deriving the bucket/region in the tool layer.
|
|
77
|
+
*/
|
|
78
|
+
publicUrl(key: string): string;
|
|
71
79
|
}
|
|
72
80
|
interface ToolContext {
|
|
73
81
|
graphql: GraphqlClient;
|
package/dist/index.js
CHANGED
|
@@ -353,6 +353,276 @@ async function uploadMedia(graphql, storage, args) {
|
|
|
353
353
|
return { media: data.createMedia, url: putResult.url };
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// src/tools/media-mapping.ts
|
|
357
|
+
var MEDIA_FIELDS = (
|
|
358
|
+
/* GraphQL */
|
|
359
|
+
`
|
|
360
|
+
fragment MediaFields on Media {
|
|
361
|
+
mediaId
|
|
362
|
+
src
|
|
363
|
+
mimeType
|
|
364
|
+
size
|
|
365
|
+
createdAt
|
|
366
|
+
updatedAt
|
|
367
|
+
}
|
|
368
|
+
`
|
|
369
|
+
);
|
|
370
|
+
function toMediaResult(row, storage) {
|
|
371
|
+
return {
|
|
372
|
+
mediaId: row.mediaId,
|
|
373
|
+
src: row.src,
|
|
374
|
+
url: storage.publicUrl(row.src),
|
|
375
|
+
mimeType: row.mimeType,
|
|
376
|
+
size: row.size,
|
|
377
|
+
createdAt: row.createdAt,
|
|
378
|
+
updatedAt: row.updatedAt
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/tools/list-media.ts
|
|
383
|
+
var QUERY2 = (
|
|
384
|
+
/* GraphQL */
|
|
385
|
+
`
|
|
386
|
+
${MEDIA_FIELDS}
|
|
387
|
+
query ListMedia($filter: ModelMediaFilterInput, $limit: Int, $nextToken: String) {
|
|
388
|
+
listMedia(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
389
|
+
items {
|
|
390
|
+
...MediaFields
|
|
391
|
+
}
|
|
392
|
+
nextToken
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
`
|
|
396
|
+
);
|
|
397
|
+
var listMediaSchema = {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
limit: {
|
|
401
|
+
type: "integer",
|
|
402
|
+
minimum: 1,
|
|
403
|
+
maximum: 100,
|
|
404
|
+
description: "Max results (default 20)"
|
|
405
|
+
},
|
|
406
|
+
nextToken: { type: "string", description: "Pagination cursor from a previous call" },
|
|
407
|
+
mimeType: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: 'Filter by MIME type prefix (beginsWith): "image/" matches all images, "image/png" matches PNG only.'
|
|
410
|
+
},
|
|
411
|
+
prefix: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: 'Filter by S3 key prefix (beginsWith) on `src`, e.g. "public/media/2024/" to find files from a given year.'
|
|
414
|
+
},
|
|
415
|
+
createdAfter: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "ISO 8601 timestamp; only return media created at or after this."
|
|
418
|
+
},
|
|
419
|
+
createdBefore: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "ISO 8601 timestamp; only return media created at or before this."
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
function buildMediaFilter(args) {
|
|
426
|
+
const conditions = [];
|
|
427
|
+
if (args.mimeType) conditions.push({ mimeType: { beginsWith: args.mimeType } });
|
|
428
|
+
if (args.prefix) conditions.push({ src: { beginsWith: args.prefix } });
|
|
429
|
+
if (args.createdAfter && args.createdBefore) {
|
|
430
|
+
conditions.push({ createdAt: { between: [args.createdAfter, args.createdBefore] } });
|
|
431
|
+
} else if (args.createdAfter) {
|
|
432
|
+
conditions.push({ createdAt: { ge: args.createdAfter } });
|
|
433
|
+
} else if (args.createdBefore) {
|
|
434
|
+
conditions.push({ createdAt: { le: args.createdBefore } });
|
|
435
|
+
}
|
|
436
|
+
if (conditions.length === 0) return void 0;
|
|
437
|
+
if (conditions.length === 1) return conditions[0];
|
|
438
|
+
return { and: conditions };
|
|
439
|
+
}
|
|
440
|
+
async function listMedia(graphql, storage, args = {}) {
|
|
441
|
+
const filter = buildMediaFilter(args);
|
|
442
|
+
const data = await graphql.query(QUERY2, {
|
|
443
|
+
filter,
|
|
444
|
+
limit: args.limit ?? 20,
|
|
445
|
+
nextToken: args.nextToken
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
media: data.listMedia.items.map((row) => toMediaResult(row, storage)),
|
|
449
|
+
nextToken: data.listMedia.nextToken
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/tools/search-media.ts
|
|
454
|
+
var QUERY3 = (
|
|
455
|
+
/* GraphQL */
|
|
456
|
+
`
|
|
457
|
+
${MEDIA_FIELDS}
|
|
458
|
+
query SearchMedia($filter: ModelMediaFilterInput, $limit: Int, $nextToken: String) {
|
|
459
|
+
listMedia(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
460
|
+
items {
|
|
461
|
+
...MediaFields
|
|
462
|
+
}
|
|
463
|
+
nextToken
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
`
|
|
467
|
+
);
|
|
468
|
+
var MAX_PAGES = 20;
|
|
469
|
+
var searchMediaSchema = {
|
|
470
|
+
type: "object",
|
|
471
|
+
required: ["query"],
|
|
472
|
+
properties: {
|
|
473
|
+
query: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: 'Substring matched (case-sensitive `contains`) against `src` (which includes the filename) and `mimeType`. e.g. "logo", ".png", "image/png".'
|
|
476
|
+
},
|
|
477
|
+
limit: {
|
|
478
|
+
type: "integer",
|
|
479
|
+
minimum: 1,
|
|
480
|
+
maximum: 100,
|
|
481
|
+
description: "Target number of matches to collect before stopping (default 20). A soft target \u2014 the last page may push the result slightly past it."
|
|
482
|
+
},
|
|
483
|
+
nextToken: { type: "string", description: "Pagination cursor from a previous call" }
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
async function searchMedia(graphql, storage, args) {
|
|
487
|
+
if (!args.query) {
|
|
488
|
+
throw new Error("search_media: `query` is required");
|
|
489
|
+
}
|
|
490
|
+
const limit = args.limit ?? 20;
|
|
491
|
+
const filter = {
|
|
492
|
+
or: [{ src: { contains: args.query } }, { mimeType: { contains: args.query } }]
|
|
493
|
+
};
|
|
494
|
+
const media = [];
|
|
495
|
+
let token = args.nextToken ?? null;
|
|
496
|
+
let pages = 0;
|
|
497
|
+
do {
|
|
498
|
+
const data = await graphql.query(QUERY3, {
|
|
499
|
+
filter,
|
|
500
|
+
limit,
|
|
501
|
+
nextToken: token ?? void 0
|
|
502
|
+
});
|
|
503
|
+
pages += 1;
|
|
504
|
+
for (const row of data.listMedia.items) {
|
|
505
|
+
media.push(toMediaResult(row, storage));
|
|
506
|
+
}
|
|
507
|
+
token = data.listMedia.nextToken;
|
|
508
|
+
} while (media.length < limit && token && pages < MAX_PAGES);
|
|
509
|
+
const truncated = media.length < limit && token !== null && pages >= MAX_PAGES;
|
|
510
|
+
return {
|
|
511
|
+
media,
|
|
512
|
+
nextToken: token,
|
|
513
|
+
...truncated ? { truncated: true } : {}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/tools/delete-media.ts
|
|
518
|
+
var GET_BY_ID2 = (
|
|
519
|
+
/* GraphQL */
|
|
520
|
+
`
|
|
521
|
+
query GetMedia($mediaId: ID!) {
|
|
522
|
+
getMedia(mediaId: $mediaId) {
|
|
523
|
+
mediaId
|
|
524
|
+
src
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
`
|
|
528
|
+
);
|
|
529
|
+
var GET_BY_SRC = (
|
|
530
|
+
/* GraphQL */
|
|
531
|
+
`
|
|
532
|
+
query GetMediaBySrc($src: String!) {
|
|
533
|
+
getMediaBySrc(src: $src) {
|
|
534
|
+
mediaId
|
|
535
|
+
src
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
`
|
|
539
|
+
);
|
|
540
|
+
var DELETE = (
|
|
541
|
+
/* GraphQL */
|
|
542
|
+
`
|
|
543
|
+
mutation DeleteMedia($input: DeleteMediaInput!) {
|
|
544
|
+
deleteMedia(input: $input) {
|
|
545
|
+
mediaId
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
`
|
|
549
|
+
);
|
|
550
|
+
var deleteMediaSchema = {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {
|
|
553
|
+
mediaId: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Media row primary key (e.g. 'media-1714400000000-abc123' as returned by `upload_media`). Provide either `mediaId` or `src`; mediaId is the direct path."
|
|
556
|
+
},
|
|
557
|
+
src: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Full S3 key including `public/` (e.g. 'public/media/2026/05/1714400000000-photo.jpg'). Use when you only have the public URL \u2014 strip `/api/media/` and prepend `public/`. Provide either `mediaId` or `src`."
|
|
560
|
+
},
|
|
561
|
+
dryRun: {
|
|
562
|
+
type: "boolean",
|
|
563
|
+
description: "When true, resolve the target but delete nothing (no S3 delete, no row delete). Returns `{ deleted: false, dryRun: true, ... }` previewing what would be removed. Use it to confirm before the real delete."
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
async function deleteMedia(graphql, storage, args) {
|
|
568
|
+
if (!args.mediaId && !args.src) {
|
|
569
|
+
throw new Error("delete_media: provide `mediaId` or `src`");
|
|
570
|
+
}
|
|
571
|
+
const dryRun = args.dryRun === true;
|
|
572
|
+
let mediaId;
|
|
573
|
+
let src;
|
|
574
|
+
if (args.mediaId) {
|
|
575
|
+
const data = await graphql.query(GET_BY_ID2, { mediaId: args.mediaId });
|
|
576
|
+
if (data.getMedia) {
|
|
577
|
+
mediaId = data.getMedia.mediaId;
|
|
578
|
+
src = data.getMedia.src;
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
const data = await graphql.query(GET_BY_SRC, { src: args.src });
|
|
582
|
+
if (data.getMediaBySrc) {
|
|
583
|
+
mediaId = data.getMediaBySrc.mediaId;
|
|
584
|
+
src = data.getMediaBySrc.src;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!mediaId) {
|
|
588
|
+
if (args.src) {
|
|
589
|
+
if (dryRun) {
|
|
590
|
+
return {
|
|
591
|
+
deleted: false,
|
|
592
|
+
dryRun: true,
|
|
593
|
+
reason: "dry run \u2014 media row not found; would attempt S3 object delete",
|
|
594
|
+
src: args.src
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
await storage.deleteObject(args.src);
|
|
598
|
+
return {
|
|
599
|
+
deleted: false,
|
|
600
|
+
reason: "media row not found; S3 object delete attempted (idempotent)",
|
|
601
|
+
src: args.src
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
deleted: false,
|
|
606
|
+
...dryRun ? { dryRun: true } : {},
|
|
607
|
+
reason: dryRun ? "dry run \u2014 media row not found for mediaId; nothing to delete" : "media row not found for mediaId"
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (dryRun) {
|
|
611
|
+
return {
|
|
612
|
+
deleted: false,
|
|
613
|
+
dryRun: true,
|
|
614
|
+
reason: "dry run \u2014 nothing deleted",
|
|
615
|
+
mediaId,
|
|
616
|
+
src
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
await storage.deleteObject(src);
|
|
620
|
+
await graphql.query(DELETE, {
|
|
621
|
+
input: { mediaId }
|
|
622
|
+
});
|
|
623
|
+
return { deleted: true, mediaId, src };
|
|
624
|
+
}
|
|
625
|
+
|
|
356
626
|
// src/tools/get-schema.ts
|
|
357
627
|
var getSchemaSchema = {
|
|
358
628
|
type: "object",
|
|
@@ -1309,6 +1579,29 @@ var tools = [
|
|
|
1309
1579
|
inputSchema: uploadMediaSchema,
|
|
1310
1580
|
handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), args)
|
|
1311
1581
|
},
|
|
1582
|
+
{
|
|
1583
|
+
name: "list_media",
|
|
1584
|
+
description: 'List media files in the CMS. Optional filters: `mimeType` (prefix match \u2014 "image/" matches all images, "image/png" only PNG), `prefix` (S3 key prefix on `src`, e.g. "public/media/2024/"), `createdAfter` / `createdBefore` (ISO 8601 date range). Returns up to `limit` rows (default 20) \u2014 each `{ mediaId, src, url, mimeType, size, createdAt, updatedAt }` \u2014 plus a `nextToken` cursor. Note: filters apply after the page read, so a page may return fewer than `limit` rows while a `nextToken` remains \u2014 follow the cursor. Use this to find media to delete without remembering `upload_media` responses.',
|
|
1585
|
+
inputSchema: listMediaSchema,
|
|
1586
|
+
handler: (args, ctx) => listMedia(ctx.graphql, ctx.storage(), args)
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "search_media",
|
|
1590
|
+
description: 'Search media by substring (case-sensitive `contains`) across `src` (which includes the filename) and `mimeType`. Pass `query` (e.g. "logo", ".png", "image/png"). Walks DynamoDB pages internally until it collects at least `limit` matches (default 20), exhausts the table, or hits its page cap \u2014 so `limit` is a soft target and the result may run slightly past it. Returns the same row shape as `list_media` plus `nextToken`; `truncated: true` means the page cap was hit with more to scan (pass `nextToken` back to continue).',
|
|
1591
|
+
inputSchema: searchMediaSchema,
|
|
1592
|
+
handler: (args, ctx) => searchMedia(ctx.graphql, ctx.storage(), args)
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
name: "delete_media",
|
|
1596
|
+
description: "Delete a media file: removes the S3 object and the Media row. Pass `mediaId` (from `upload_media`'s response, or `list_media` / `search_media`) or `src` (full S3 key like `public/media/2026/05/...`). When only `src` is given, looks up the Media row via `getMediaBySrc`. S3 delete runs first, then the DDB row delete \u2014 both are idempotent so re-running converges on missing-key cases. Pass `dryRun: true` to preview what would be deleted without touching anything (returns `{ deleted: false, dryRun: true, ... }`). Returns `{ deleted: false }` instead of throwing when no Media row matches; if `src` was supplied directly the S3 object is still removed (use this to sweep orphan files).",
|
|
1597
|
+
inputSchema: deleteMediaSchema,
|
|
1598
|
+
handler: (args, ctx) => deleteMedia(
|
|
1599
|
+
ctx.graphql,
|
|
1600
|
+
ctx.storage(),
|
|
1601
|
+
args
|
|
1602
|
+
),
|
|
1603
|
+
destructive: true
|
|
1604
|
+
},
|
|
1312
1605
|
{
|
|
1313
1606
|
name: "get_schema",
|
|
1314
1607
|
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.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/mcp-server",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.26",
|
|
4
4
|
"description": "MCP tool registry shared by @ampless/backend mcp-handler Lambda. Installed transitively via @ampless/admin / @ampless/backend; no direct install needed.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|