applaunchflow 0.3.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/README.md +110 -0
- package/build/catalog.js +5 -0
- package/build/catalog.test.js +13 -0
- package/build/cli-core.js +20 -0
- package/build/cli-core.test.js +24 -0
- package/build/cli.js +71 -0
- package/build/client/api.js +341 -0
- package/build/http.js +178 -0
- package/build/http.test.js +111 -0
- package/build/index.js +127 -0
- package/build/prompts/register.js +66 -0
- package/build/read-receipt.test.js +36 -0
- package/build/resources/data.js +723 -0
- package/build/resources/register.js +115 -0
- package/build/social-template-previews.js +84 -0
- package/build/template-previews.js +83 -0
- package/build/tool-metadata.js +112 -0
- package/build/tool-metadata.test.js +32 -0
- package/build/tools/assets.js +311 -0
- package/build/tools/graphics.js +475 -0
- package/build/tools/keywords.js +132 -0
- package/build/tools/layouts.js +283 -0
- package/build/tools/localization.js +59 -0
- package/build/tools/mockups.js +324 -0
- package/build/tools/projects.js +113 -0
- package/build/tools/promovideo.js +199 -0
- package/build/tools/screenshots.js +307 -0
- package/build/tools/templates.js +195 -0
- package/build/tools/utils.js +212 -0
- package/build/tools/variants.js +71 -0
- package/package.json +33 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { buildTemplatePreviewUrl, decorateTemplatePayload, isSafeTemplateId, isTemplatePreviewDeviceType, } from "../template-previews.js";
|
|
4
|
+
import { LAYOUT_SCHEMA_RESOURCE, PROJECT_CREATION_WIZARD_RESOURCE, SUPPORTED_DEVICES, TRANSFORM_SCHEMA_RESOURCE, VIDEO_CONFIG_SCHEMA_RESOURCE, WORKFLOW_GUIDE_RESOURCE, } from "./data.js";
|
|
5
|
+
function asJsonResource(uri, payload) {
|
|
6
|
+
return {
|
|
7
|
+
contents: [
|
|
8
|
+
{
|
|
9
|
+
uri,
|
|
10
|
+
mimeType: "application/json",
|
|
11
|
+
text: JSON.stringify(payload, null, 2),
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function asBinaryResource(uri, mimeType, blob) {
|
|
17
|
+
return {
|
|
18
|
+
contents: [
|
|
19
|
+
{
|
|
20
|
+
uri,
|
|
21
|
+
mimeType,
|
|
22
|
+
blob,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function registerResources(server, client) {
|
|
28
|
+
server.registerResource("layout-schema", "applaunchflow://schema/layout", {
|
|
29
|
+
title: "Layout Schema",
|
|
30
|
+
description: "Full field-level reference for the layout JSON used by BOTH screenshots and social graphics: every node type, its properties and valid value ranges. Read before hand-writing or editing layout JSON.",
|
|
31
|
+
mimeType: "application/json",
|
|
32
|
+
}, async (uri) => asJsonResource(uri.href, LAYOUT_SCHEMA_RESOURCE));
|
|
33
|
+
server.registerResource("video-config-schema", "applaunchflow://schema/video-config", {
|
|
34
|
+
title: "Promo Video Config Schema",
|
|
35
|
+
description: "Full field-level reference for the Remotion VideoConfig: scene types and their content shapes, theme, text styles, ken burns, choreography preset ids, audio. Read before editing a promo video config.",
|
|
36
|
+
mimeType: "application/json",
|
|
37
|
+
}, async (uri) => asJsonResource(uri.href, VIDEO_CONFIG_SCHEMA_RESOURCE));
|
|
38
|
+
server.registerResource("transform-schema", "applaunchflow://schema/transforms", {
|
|
39
|
+
title: "Transform Schema",
|
|
40
|
+
description: "Supported transform_layout operations, the selector syntax (including the pitfalls that silently match every screen), and the valid node types. Read before calling transform_layout.",
|
|
41
|
+
mimeType: "application/json",
|
|
42
|
+
}, async (uri) => asJsonResource(uri.href, TRANSFORM_SCHEMA_RESOURCE));
|
|
43
|
+
server.registerResource("template-catalog", "applaunchflow://templates", {
|
|
44
|
+
title: "Template Catalog",
|
|
45
|
+
description: "Current screenshot template catalog",
|
|
46
|
+
mimeType: "application/json",
|
|
47
|
+
}, async (uri) => asJsonResource(uri.href, decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl)));
|
|
48
|
+
server.registerResource("template-preview", new ResourceTemplate("applaunchflow://templates/{templateId}/preview/{deviceType}", {
|
|
49
|
+
list: undefined,
|
|
50
|
+
}), {
|
|
51
|
+
title: "Template Preview",
|
|
52
|
+
description: "Rendered preview image for a screenshot template and device type.",
|
|
53
|
+
mimeType: "image/png",
|
|
54
|
+
}, async (uri, { templateId, deviceType }) => {
|
|
55
|
+
const resolvedTemplateId = Array.isArray(templateId)
|
|
56
|
+
? templateId[0]
|
|
57
|
+
: templateId;
|
|
58
|
+
const resolvedDeviceType = Array.isArray(deviceType)
|
|
59
|
+
? deviceType[0]
|
|
60
|
+
: deviceType;
|
|
61
|
+
if (!isSafeTemplateId(resolvedTemplateId) ||
|
|
62
|
+
!isTemplatePreviewDeviceType(resolvedDeviceType)) {
|
|
63
|
+
throw new Error("Invalid template preview request");
|
|
64
|
+
}
|
|
65
|
+
const headers = new Headers();
|
|
66
|
+
headers.set("Authorization", `Bearer ${client.credentials.token}`);
|
|
67
|
+
const previewUrl = buildTemplatePreviewUrl(client.credentials.baseUrl, resolvedTemplateId, resolvedDeviceType);
|
|
68
|
+
const response = await fetch(previewUrl, {
|
|
69
|
+
headers,
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Failed to fetch template preview ${resolvedTemplateId}/${resolvedDeviceType}: HTTP ${response.status}`);
|
|
73
|
+
}
|
|
74
|
+
const mimeType = response.headers.get("content-type") || "image/png";
|
|
75
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
76
|
+
const blob = Buffer.from(arrayBuffer).toString("base64");
|
|
77
|
+
return asBinaryResource(uri.href, mimeType, blob);
|
|
78
|
+
});
|
|
79
|
+
server.registerResource("project-creation-wizard", "applaunchflow://wizard/project-creation", {
|
|
80
|
+
title: "Project Creation Wizard",
|
|
81
|
+
description: "The exact guided flow used by the UI create-project wizard",
|
|
82
|
+
mimeType: "application/json",
|
|
83
|
+
}, async (uri) => asJsonResource(uri.href, PROJECT_CREATION_WIZARD_RESOURCE));
|
|
84
|
+
server.registerResource("workflow-guide", "applaunchflow://guide/workflows", {
|
|
85
|
+
title: "Workflow Guide",
|
|
86
|
+
description: "Preferred MCP workflows for generating and editing projects",
|
|
87
|
+
mimeType: "application/json",
|
|
88
|
+
}, async (uri) => asJsonResource(uri.href, WORKFLOW_GUIDE_RESOURCE));
|
|
89
|
+
server.registerResource("devices", "applaunchflow://devices", {
|
|
90
|
+
title: "Supported Devices",
|
|
91
|
+
description: "Device frame identifiers and dimensions",
|
|
92
|
+
mimeType: "application/json",
|
|
93
|
+
}, async (uri) => asJsonResource(uri.href, SUPPORTED_DEVICES));
|
|
94
|
+
server.registerResource("project-state", new ResourceTemplate("applaunchflow://project/{projectId}/state", {
|
|
95
|
+
list: undefined,
|
|
96
|
+
}), {
|
|
97
|
+
title: "Project State",
|
|
98
|
+
description: "Project state summary with variants and translations",
|
|
99
|
+
mimeType: "application/json",
|
|
100
|
+
}, async (uri, { projectId }) => {
|
|
101
|
+
const resolvedProjectId = Array.isArray(projectId) ? projectId[0] : projectId;
|
|
102
|
+
const [project, translations, screenshotVariants] = await Promise.all([
|
|
103
|
+
client.getProject(resolvedProjectId),
|
|
104
|
+
client.getLayout({ generationId: resolvedProjectId }),
|
|
105
|
+
client.listVariants(resolvedProjectId, "screenshots"),
|
|
106
|
+
]);
|
|
107
|
+
return asJsonResource(uri.href, {
|
|
108
|
+
project,
|
|
109
|
+
translations,
|
|
110
|
+
variants: {
|
|
111
|
+
screenshots: screenshotVariants,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const SOCIAL_FORMATS = [
|
|
2
|
+
"og",
|
|
3
|
+
"x_post",
|
|
4
|
+
"instagram_story",
|
|
5
|
+
"play_store_feature",
|
|
6
|
+
"x_header",
|
|
7
|
+
"linkedin_banner",
|
|
8
|
+
];
|
|
9
|
+
export function isSocialFormat(value) {
|
|
10
|
+
return SOCIAL_FORMATS.includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function buildSocialTemplatePreviewPath(templateId, format) {
|
|
13
|
+
return `/template-examples/social/${format}/${templateId}.png`;
|
|
14
|
+
}
|
|
15
|
+
export function buildSocialTemplatePreviewUrl(baseUrl, templateId, format) {
|
|
16
|
+
return new URL(buildSocialTemplatePreviewPath(templateId, format), baseUrl).toString();
|
|
17
|
+
}
|
|
18
|
+
export function buildSocialTemplatePreviewResourceUri(templateId, format) {
|
|
19
|
+
return `applaunchflow://social-templates/${encodeURIComponent(templateId)}/preview/${format}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildSocialTemplateGalleryUrl(baseUrl, options) {
|
|
22
|
+
const url = new URL("/template-gallery", baseUrl);
|
|
23
|
+
url.searchParams.set("kind", "social");
|
|
24
|
+
if (options?.format) {
|
|
25
|
+
url.searchParams.set("format", options.format);
|
|
26
|
+
}
|
|
27
|
+
if (options?.templateIds?.length) {
|
|
28
|
+
url.searchParams.set("ids", options.templateIds.join(","));
|
|
29
|
+
}
|
|
30
|
+
if (options?.selectedTemplateId) {
|
|
31
|
+
url.searchParams.set("selected", options.selectedTemplateId);
|
|
32
|
+
}
|
|
33
|
+
if (options?.title) {
|
|
34
|
+
url.searchParams.set("title", options.title);
|
|
35
|
+
}
|
|
36
|
+
if (options?.returnTo) {
|
|
37
|
+
url.searchParams.set("returnTo", options.returnTo);
|
|
38
|
+
}
|
|
39
|
+
if (options?.generationId && options?.catalogKey) {
|
|
40
|
+
url.searchParams.set("generationId", options.generationId);
|
|
41
|
+
url.searchParams.set("catalogKey", options.catalogKey);
|
|
42
|
+
}
|
|
43
|
+
return url.toString();
|
|
44
|
+
}
|
|
45
|
+
function decorateSocialTemplate(template, baseUrl) {
|
|
46
|
+
const previewUrls = Object.fromEntries(SOCIAL_FORMATS.map((format) => [
|
|
47
|
+
format,
|
|
48
|
+
template.previewUrls?.[format] ||
|
|
49
|
+
buildSocialTemplatePreviewUrl(baseUrl, template.id, format),
|
|
50
|
+
]));
|
|
51
|
+
const previewResourceUris = Object.fromEntries(SOCIAL_FORMATS.map((format) => [
|
|
52
|
+
format,
|
|
53
|
+
buildSocialTemplatePreviewResourceUri(template.id, format),
|
|
54
|
+
]));
|
|
55
|
+
return {
|
|
56
|
+
...template,
|
|
57
|
+
previewUrls,
|
|
58
|
+
previewResourceUris,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function decorateSocialTemplatePayload(payload, baseUrl) {
|
|
62
|
+
if (payload &&
|
|
63
|
+
typeof payload === "object" &&
|
|
64
|
+
"templates" in payload &&
|
|
65
|
+
Array.isArray(payload.templates)) {
|
|
66
|
+
const typedPayload = payload;
|
|
67
|
+
return {
|
|
68
|
+
...typedPayload,
|
|
69
|
+
templates: typedPayload.templates.map((template) => decorateSocialTemplate(template, baseUrl)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (payload &&
|
|
73
|
+
typeof payload === "object" &&
|
|
74
|
+
"template" in payload &&
|
|
75
|
+
payload.template &&
|
|
76
|
+
typeof payload.template === "object") {
|
|
77
|
+
const typedPayload = payload;
|
|
78
|
+
return {
|
|
79
|
+
...typedPayload,
|
|
80
|
+
template: decorateSocialTemplate(typedPayload.template, baseUrl),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return payload;
|
|
84
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const TEMPLATE_PREVIEW_DEVICE_TYPES = [
|
|
2
|
+
"phone",
|
|
3
|
+
"tablet",
|
|
4
|
+
"desktop",
|
|
5
|
+
];
|
|
6
|
+
export function isTemplatePreviewDeviceType(value) {
|
|
7
|
+
return TEMPLATE_PREVIEW_DEVICE_TYPES.includes(value);
|
|
8
|
+
}
|
|
9
|
+
export function isSafeTemplateId(templateId) {
|
|
10
|
+
return /^[a-z0-9-]+$/i.test(templateId);
|
|
11
|
+
}
|
|
12
|
+
export function buildTemplatePreviewPath(templateId, deviceType) {
|
|
13
|
+
return `/template-examples/${deviceType}/${templateId}.png`;
|
|
14
|
+
}
|
|
15
|
+
export function buildTemplatePreviewUrl(baseUrl, templateId, deviceType) {
|
|
16
|
+
return new URL(buildTemplatePreviewPath(templateId, deviceType), baseUrl).toString();
|
|
17
|
+
}
|
|
18
|
+
export function buildTemplatePreviewResourceUri(templateId, deviceType) {
|
|
19
|
+
return `applaunchflow://templates/${encodeURIComponent(templateId)}/preview/${deviceType}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildTemplateGalleryUrl(baseUrl, options) {
|
|
22
|
+
const url = new URL("/template-gallery", baseUrl);
|
|
23
|
+
if (options?.deviceType) {
|
|
24
|
+
url.searchParams.set("device", options.deviceType);
|
|
25
|
+
}
|
|
26
|
+
if (options?.templateIds?.length) {
|
|
27
|
+
url.searchParams.set("ids", options.templateIds.join(","));
|
|
28
|
+
}
|
|
29
|
+
if (options?.selectedTemplateId) {
|
|
30
|
+
url.searchParams.set("selected", options.selectedTemplateId);
|
|
31
|
+
}
|
|
32
|
+
if (options?.title) {
|
|
33
|
+
url.searchParams.set("title", options.title);
|
|
34
|
+
}
|
|
35
|
+
if (options?.returnTo) {
|
|
36
|
+
url.searchParams.set("returnTo", options.returnTo);
|
|
37
|
+
}
|
|
38
|
+
if (options?.generationId && options?.catalogKey) {
|
|
39
|
+
url.searchParams.set("generationId", options.generationId);
|
|
40
|
+
url.searchParams.set("catalogKey", options.catalogKey);
|
|
41
|
+
}
|
|
42
|
+
return url.toString();
|
|
43
|
+
}
|
|
44
|
+
function decorateTemplate(template, baseUrl) {
|
|
45
|
+
const previewUrls = Object.fromEntries(TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => [
|
|
46
|
+
deviceType,
|
|
47
|
+
template.previewUrls?.[deviceType] ||
|
|
48
|
+
buildTemplatePreviewUrl(baseUrl, template.id, deviceType),
|
|
49
|
+
]));
|
|
50
|
+
const previewResourceUris = Object.fromEntries(TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => [
|
|
51
|
+
deviceType,
|
|
52
|
+
buildTemplatePreviewResourceUri(template.id, deviceType),
|
|
53
|
+
]));
|
|
54
|
+
return {
|
|
55
|
+
...template,
|
|
56
|
+
previewUrls,
|
|
57
|
+
previewResourceUris,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function decorateTemplatePayload(payload, baseUrl) {
|
|
61
|
+
if (payload &&
|
|
62
|
+
typeof payload === "object" &&
|
|
63
|
+
"templates" in payload &&
|
|
64
|
+
Array.isArray(payload.templates)) {
|
|
65
|
+
const typedPayload = payload;
|
|
66
|
+
return {
|
|
67
|
+
...typedPayload,
|
|
68
|
+
templates: typedPayload.templates.map((template) => decorateTemplate(template, baseUrl)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (payload &&
|
|
72
|
+
typeof payload === "object" &&
|
|
73
|
+
"template" in payload &&
|
|
74
|
+
payload.template &&
|
|
75
|
+
typeof payload.template === "object") {
|
|
76
|
+
const typedPayload = payload;
|
|
77
|
+
return {
|
|
78
|
+
...typedPayload,
|
|
79
|
+
template: decorateTemplate(typedPayload.template, baseUrl),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const readOnly = {
|
|
3
|
+
readOnlyHint: true,
|
|
4
|
+
destructiveHint: false,
|
|
5
|
+
idempotentHint: true,
|
|
6
|
+
openWorldHint: false,
|
|
7
|
+
};
|
|
8
|
+
const createOrAppend = {
|
|
9
|
+
readOnlyHint: false,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: false,
|
|
12
|
+
openWorldHint: false,
|
|
13
|
+
};
|
|
14
|
+
const overwriteOrDelete = {
|
|
15
|
+
readOnlyHint: false,
|
|
16
|
+
destructiveHint: true,
|
|
17
|
+
idempotentHint: false,
|
|
18
|
+
openWorldHint: false,
|
|
19
|
+
};
|
|
20
|
+
const openWorldCreate = {
|
|
21
|
+
...createOrAppend,
|
|
22
|
+
openWorldHint: true,
|
|
23
|
+
};
|
|
24
|
+
export const TOOL_ANNOTATIONS = {
|
|
25
|
+
list_projects: readOnly,
|
|
26
|
+
get_project: readOnly,
|
|
27
|
+
create_project: createOrAppend,
|
|
28
|
+
delete_project: overwriteOrDelete,
|
|
29
|
+
upload_screenshots: openWorldCreate,
|
|
30
|
+
list_illustrations: readOnly,
|
|
31
|
+
upload_asset: openWorldCreate,
|
|
32
|
+
prepare_screenshot_styles: createOrAppend,
|
|
33
|
+
apply_screenshot_style: createOrAppend,
|
|
34
|
+
generate_layouts: createOrAppend,
|
|
35
|
+
list_source_screenshots: readOnly,
|
|
36
|
+
list_screenshots: readOnly,
|
|
37
|
+
view_screenshot: readOnly,
|
|
38
|
+
get_layout: readOnly,
|
|
39
|
+
save_layout: overwriteOrDelete,
|
|
40
|
+
transform_layout: overwriteOrDelete,
|
|
41
|
+
browse_templates: readOnly,
|
|
42
|
+
prepare_social_graphics_styles: createOrAppend,
|
|
43
|
+
apply_social_graphics_style: createOrAppend,
|
|
44
|
+
browse_social_templates: readOnly,
|
|
45
|
+
generate_graphics: createOrAppend,
|
|
46
|
+
get_graphics: readOnly,
|
|
47
|
+
get_graphics_format: readOnly,
|
|
48
|
+
save_graphics: overwriteOrDelete,
|
|
49
|
+
save_graphics_format: overwriteOrDelete,
|
|
50
|
+
generate_promo_video: createOrAppend,
|
|
51
|
+
get_promo_video: readOnly,
|
|
52
|
+
update_promo_video: overwriteOrDelete,
|
|
53
|
+
clear_promo_video: overwriteOrDelete,
|
|
54
|
+
create_mockup_animation: createOrAppend,
|
|
55
|
+
get_mockup_animation: readOnly,
|
|
56
|
+
update_mockup_animation: overwriteOrDelete,
|
|
57
|
+
list_mockup_media: readOnly,
|
|
58
|
+
list_mockup_presets: readOnly,
|
|
59
|
+
translate_layouts: createOrAppend,
|
|
60
|
+
list_translations: readOnly,
|
|
61
|
+
list_variants: readOnly,
|
|
62
|
+
create_variant: createOrAppend,
|
|
63
|
+
duplicate_variant: createOrAppend,
|
|
64
|
+
list_keywords: readOnly,
|
|
65
|
+
list_keyword_competitors: readOnly,
|
|
66
|
+
add_keywords: createOrAppend,
|
|
67
|
+
get_keyword_history: readOnly,
|
|
68
|
+
};
|
|
69
|
+
const OAUTH_SECURITY_SCHEME = {
|
|
70
|
+
type: "oauth2",
|
|
71
|
+
scopes: [
|
|
72
|
+
"projects:read",
|
|
73
|
+
"projects:write",
|
|
74
|
+
"assets:write",
|
|
75
|
+
"generations:write",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const GENERIC_OUTPUT_SCHEMA = {
|
|
79
|
+
success: z.boolean(),
|
|
80
|
+
data: z.unknown().optional(),
|
|
81
|
+
message: z.string().optional(),
|
|
82
|
+
error: z.unknown().optional(),
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Apply the submission metadata contract centrally so local and hosted
|
|
86
|
+
* transports cannot silently drift. Registration throws when a new tool is
|
|
87
|
+
* added without an explicit safety classification.
|
|
88
|
+
*/
|
|
89
|
+
export function installToolMetadataPolicy(server, options = {}) {
|
|
90
|
+
const registerTool = server.registerTool.bind(server);
|
|
91
|
+
server.registerTool = ((name, config, callback) => {
|
|
92
|
+
const annotations = TOOL_ANNOTATIONS[name];
|
|
93
|
+
if (!annotations) {
|
|
94
|
+
throw new Error(`Missing tool safety annotations for ${name}`);
|
|
95
|
+
}
|
|
96
|
+
const existingMeta = (config._meta || {});
|
|
97
|
+
return registerTool(name, {
|
|
98
|
+
...config,
|
|
99
|
+
annotations: {
|
|
100
|
+
...(config.annotations || {}),
|
|
101
|
+
...annotations,
|
|
102
|
+
},
|
|
103
|
+
outputSchema: config.outputSchema || GENERIC_OUTPUT_SCHEMA,
|
|
104
|
+
_meta: {
|
|
105
|
+
...existingMeta,
|
|
106
|
+
...(options.hosted
|
|
107
|
+
? { securitySchemes: [OAUTH_SECURITY_SCHEME] }
|
|
108
|
+
: {}),
|
|
109
|
+
},
|
|
110
|
+
}, callback);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
5
|
+
import { createAppLaunchFlowServer } from "./index.js";
|
|
6
|
+
import { TOOL_ANNOTATIONS } from "./tool-metadata.js";
|
|
7
|
+
test("all registered tools expose submission safety metadata and output schemas", async () => {
|
|
8
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
9
|
+
const server = createAppLaunchFlowServer({
|
|
10
|
+
baseUrl: "https://dashboard.applaunchflow.com",
|
|
11
|
+
token: "test-token",
|
|
12
|
+
});
|
|
13
|
+
const client = new Client({ name: "metadata-test", version: "1.0.0" });
|
|
14
|
+
await server.connect(serverTransport);
|
|
15
|
+
await client.connect(clientTransport);
|
|
16
|
+
try {
|
|
17
|
+
const { tools } = await client.listTools();
|
|
18
|
+
assert.equal(tools.length, 43);
|
|
19
|
+
assert.deepEqual(tools.map((tool) => tool.name).sort(), Object.keys(TOOL_ANNOTATIONS).sort());
|
|
20
|
+
for (const tool of tools) {
|
|
21
|
+
assert.deepEqual(tool.annotations, TOOL_ANNOTATIONS[tool.name]);
|
|
22
|
+
assert.equal(tool.outputSchema?.type, "object");
|
|
23
|
+
assert.ok(tool.title, `${tool.name} must have a title`);
|
|
24
|
+
assert.ok(tool.description, `${tool.name} must have a description`);
|
|
25
|
+
assert.deepEqual(tool._meta?.securitySchemes?.map((scheme) => scheme.type), ["oauth2"]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await client.close();
|
|
30
|
+
await server.close();
|
|
31
|
+
}
|
|
32
|
+
});
|