@clipform/mcp-server 1.9.2 → 1.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/{chunk-ESDD3WVD.js → chunk-62IK3CBP.js} +117 -4
- package/dist/chunk-62IK3CBP.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +13 -4
- package/dist/chunk-ESDD3WVD.js.map +0 -1
package/README.md
CHANGED
|
@@ -77,15 +77,16 @@ You can also pass the key as a CLI flag: `npx -y @clipform/mcp-server --api-key=
|
|
|
77
77
|
|
|
78
78
|
| Tool | Description |
|
|
79
79
|
|------|-------------|
|
|
80
|
-
| `clipform_render_composition` | Render a
|
|
80
|
+
| `clipform_render_composition` | Render a video composition to MP4, PNG, or GIF (async - returns job ID) |
|
|
81
81
|
| `clipform_generate_tts` | Generate narration audio with word-level captions |
|
|
82
82
|
| `clipform_generate_slideshow` | Create slideshow videos from images + audio (async - returns job ID) |
|
|
83
83
|
| `clipform_generate_video` | Generate video from images, clips, or both synced to audio (async - returns job ID) |
|
|
84
84
|
| `clipform_check_render` | Check status of an async render job |
|
|
85
85
|
| `clipform_search_media` | Search royalty-free images and videos |
|
|
86
86
|
| `clipform_search_music` | Search royalty-free music and ambient sounds |
|
|
87
|
-
| `clipform_list_compositions` | List available
|
|
87
|
+
| `clipform_list_compositions` | List available video compositions and prop schemas |
|
|
88
88
|
| `clipform_list_assets` | List available sound effects, animations, and fonts |
|
|
89
|
+
| `clipform_fetch_boundary` | Fetch a GeoJSON boundary polygon for a country, city, or region |
|
|
89
90
|
|
|
90
91
|
## Example
|
|
91
92
|
|
|
@@ -23593,7 +23593,7 @@ function registerRenderCompositionTool(server) {
|
|
|
23593
23593
|
"clipform_render_composition",
|
|
23594
23594
|
{
|
|
23595
23595
|
title: "Render Composition",
|
|
23596
|
-
description: `Render a
|
|
23596
|
+
description: `Render a video composition to MP4, PNG, or GIF.
|
|
23597
23597
|
|
|
23598
23598
|
Output formats:
|
|
23599
23599
|
- mp4: Video file (H.264 codec, best for social media)
|
|
@@ -23685,7 +23685,7 @@ function registerListCompositionsTool(server) {
|
|
|
23685
23685
|
"clipform_list_compositions",
|
|
23686
23686
|
{
|
|
23687
23687
|
title: "List Compositions",
|
|
23688
|
-
description: `List all available
|
|
23688
|
+
description: `List all available video compositions and their expected props schemas. Use clipform_render_composition to render them.`,
|
|
23689
23689
|
inputSchema: {},
|
|
23690
23690
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
23691
23691
|
},
|
|
@@ -23695,7 +23695,7 @@ function registerListCompositionsTool(server) {
|
|
|
23695
23695
|
});
|
|
23696
23696
|
if (!result.ok) {
|
|
23697
23697
|
if (result.status === 0 || result.status === 404) {
|
|
23698
|
-
return errorResult("
|
|
23698
|
+
return errorResult("Video compositions are not available (API server may not be running). Start the API with 'npm run dev --workspace=apps/api'.");
|
|
23699
23699
|
}
|
|
23700
23700
|
return errorResult(result.error);
|
|
23701
23701
|
}
|
|
@@ -23947,6 +23947,118 @@ Returns the current status and, when complete, the output URL. If still renderin
|
|
|
23947
23947
|
);
|
|
23948
23948
|
}
|
|
23949
23949
|
|
|
23950
|
+
// src/tools/fetch-boundary.ts
|
|
23951
|
+
var NOMINATIM_BASE = "https://nominatim.openstreetmap.org/search";
|
|
23952
|
+
var USER_AGENT = "Clipform/1.0 (https://clipform.io)";
|
|
23953
|
+
function polygonArea(ring) {
|
|
23954
|
+
let area = 0;
|
|
23955
|
+
for (let i = 0; i < ring.length - 1; i++) {
|
|
23956
|
+
area += ring[i][0] * ring[i + 1][1];
|
|
23957
|
+
area -= ring[i + 1][0] * ring[i][1];
|
|
23958
|
+
}
|
|
23959
|
+
return Math.abs(area / 2);
|
|
23960
|
+
}
|
|
23961
|
+
function simplifyRing(ring, maxPoints) {
|
|
23962
|
+
if (ring.length <= maxPoints) return ring;
|
|
23963
|
+
const step = Math.max(1, Math.floor(ring.length / maxPoints));
|
|
23964
|
+
const result = [];
|
|
23965
|
+
for (let i = 0; i < ring.length - 1; i += step) {
|
|
23966
|
+
result.push(ring[i]);
|
|
23967
|
+
}
|
|
23968
|
+
result.push(ring[ring.length - 1]);
|
|
23969
|
+
return result;
|
|
23970
|
+
}
|
|
23971
|
+
function registerFetchBoundaryTool(server) {
|
|
23972
|
+
server.registerTool(
|
|
23973
|
+
"clipform_fetch_boundary",
|
|
23974
|
+
{
|
|
23975
|
+
title: "Fetch Geographic Boundary",
|
|
23976
|
+
description: `Fetch a GeoJSON boundary polygon for a country, city, or region. Returns simplified GeoJSON ready to use as the 'boundary' prop in GlobeToCity/CityToGlobe compositions.
|
|
23977
|
+
|
|
23978
|
+
Use mainlandOnly to exclude small islands and overseas territories (e.g. Corsica for France, Hawaii for USA).`,
|
|
23979
|
+
inputSchema: {
|
|
23980
|
+
query: external_exports.string().describe("Place name (e.g. 'France', 'Paris', 'Tokyo', 'Brazil')"),
|
|
23981
|
+
type: external_exports.enum(["country", "city", "state", "region"]).default("country").describe("Type of place to search for"),
|
|
23982
|
+
mainlandOnly: external_exports.boolean().default(true).describe("Keep only the largest landmass, excluding small islands and overseas territories"),
|
|
23983
|
+
maxPoints: external_exports.number().default(500).describe("Maximum points per polygon ring for simplification (default 500, lower = smaller payload)")
|
|
23984
|
+
},
|
|
23985
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
23986
|
+
},
|
|
23987
|
+
async ({ query, type, mainlandOnly, maxPoints }) => {
|
|
23988
|
+
const params = new URLSearchParams({
|
|
23989
|
+
format: "geojson",
|
|
23990
|
+
polygon_geojson: "1",
|
|
23991
|
+
polygon_threshold: "0.001",
|
|
23992
|
+
limit: "1"
|
|
23993
|
+
});
|
|
23994
|
+
if (type === "country") {
|
|
23995
|
+
params.set("country", query);
|
|
23996
|
+
} else if (type === "city") {
|
|
23997
|
+
params.set("city", query);
|
|
23998
|
+
} else {
|
|
23999
|
+
params.set("q", query);
|
|
24000
|
+
}
|
|
24001
|
+
const url = `${NOMINATIM_BASE}?${params}`;
|
|
24002
|
+
const response = await fetch(url, {
|
|
24003
|
+
headers: { "User-Agent": USER_AGENT }
|
|
24004
|
+
});
|
|
24005
|
+
if (!response.ok) {
|
|
24006
|
+
return errorResult(`Nominatim API error: ${response.status} ${response.statusText}`);
|
|
24007
|
+
}
|
|
24008
|
+
const data = await response.json();
|
|
24009
|
+
if (!data.features?.length) {
|
|
24010
|
+
return errorResult(`No boundary found for "${query}" (type: ${type}). Try a different name or type.`);
|
|
24011
|
+
}
|
|
24012
|
+
const feature = data.features[0];
|
|
24013
|
+
let geometry = feature.geometry;
|
|
24014
|
+
if (geometry.type === "MultiPolygon" && mainlandOnly) {
|
|
24015
|
+
const withArea = geometry.coordinates.map((poly, i) => ({
|
|
24016
|
+
index: i,
|
|
24017
|
+
coords: poly,
|
|
24018
|
+
area: polygonArea(poly[0])
|
|
24019
|
+
}));
|
|
24020
|
+
withArea.sort((a, b) => b.area - a.area);
|
|
24021
|
+
const largestArea = withArea[0].area;
|
|
24022
|
+
const kept = withArea.filter((p) => p.area > largestArea * 0.05);
|
|
24023
|
+
geometry = {
|
|
24024
|
+
type: kept.length === 1 ? "Polygon" : "MultiPolygon",
|
|
24025
|
+
coordinates: kept.length === 1 ? kept[0].coords : kept.map((p) => p.coords)
|
|
24026
|
+
};
|
|
24027
|
+
}
|
|
24028
|
+
if (geometry.type === "Polygon") {
|
|
24029
|
+
geometry.coordinates = geometry.coordinates.map(
|
|
24030
|
+
(ring) => simplifyRing(ring, maxPoints)
|
|
24031
|
+
);
|
|
24032
|
+
} else if (geometry.type === "MultiPolygon") {
|
|
24033
|
+
geometry.coordinates = geometry.coordinates.map(
|
|
24034
|
+
(poly) => poly.map((ring) => simplifyRing(ring, maxPoints))
|
|
24035
|
+
);
|
|
24036
|
+
}
|
|
24037
|
+
const result = {
|
|
24038
|
+
type: "Feature",
|
|
24039
|
+
properties: { name: feature.properties.display_name },
|
|
24040
|
+
geometry
|
|
24041
|
+
};
|
|
24042
|
+
const geojsonStr = JSON.stringify(result);
|
|
24043
|
+
const sizeKb = (geojsonStr.length / 1024).toFixed(1);
|
|
24044
|
+
return textResult(
|
|
24045
|
+
[
|
|
24046
|
+
`Boundary fetched for: ${feature.properties.display_name}`,
|
|
24047
|
+
`Geometry: ${geometry.type}`,
|
|
24048
|
+
`Size: ${sizeKb} KB`,
|
|
24049
|
+
mainlandOnly ? `Mainland filter: applied (islands < 5% of largest landmass removed)` : `Mainland filter: off`,
|
|
24050
|
+
``,
|
|
24051
|
+
`Use this as the boundary.geojson prop in GlobeToCity:`,
|
|
24052
|
+
``,
|
|
24053
|
+
"```json",
|
|
24054
|
+
geojsonStr,
|
|
24055
|
+
"```"
|
|
24056
|
+
].join("\n")
|
|
24057
|
+
);
|
|
24058
|
+
}
|
|
24059
|
+
);
|
|
24060
|
+
}
|
|
24061
|
+
|
|
23950
24062
|
// src/lib/session-context.ts
|
|
23951
24063
|
async function getSessionContext() {
|
|
23952
24064
|
const result = await callApi("/me", { method: "GET" });
|
|
@@ -24673,6 +24785,7 @@ function createServer() {
|
|
|
24673
24785
|
registerListCompositionsTool(server);
|
|
24674
24786
|
registerListAssetsTool(server);
|
|
24675
24787
|
registerCheckRenderTool(server);
|
|
24788
|
+
registerFetchBoundaryTool(server);
|
|
24676
24789
|
registerPrompts(server);
|
|
24677
24790
|
registerResources(server);
|
|
24678
24791
|
return server;
|
|
@@ -24683,4 +24796,4 @@ export {
|
|
|
24683
24796
|
setApiKey,
|
|
24684
24797
|
createServer
|
|
24685
24798
|
};
|
|
24686
|
-
//# sourceMappingURL=chunk-
|
|
24799
|
+
//# sourceMappingURL=chunk-62IK3CBP.js.map
|