@lowdefy/server-dev 5.6.0 → 6.0.0
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/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +122 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +61 -47
- package/package.original.json +64 -50
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +235 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
import checkpointToMocks from './checkpointToMocks.js';
|
|
21
|
+
import createConfigCheckpoint from './createConfigCheckpoint.js';
|
|
22
|
+
import evalOperator from './evalOperator.js';
|
|
23
|
+
import findConfig from './findConfig.js';
|
|
24
|
+
import getAppMap from './getAppMap.js';
|
|
25
|
+
import getBuildStatus from './getBuildStatus.js';
|
|
26
|
+
import getCoreDoc from './getCoreDoc.js';
|
|
27
|
+
import getExamples from './getExamples.js';
|
|
28
|
+
import getOverview from './getOverview.js';
|
|
29
|
+
import getPageConfig from './getPageConfig.js';
|
|
30
|
+
import getPluginDoc from './getPluginDoc.js';
|
|
31
|
+
import getSchema from './getSchema.js';
|
|
32
|
+
import inspectState from './inspectState.js';
|
|
33
|
+
import listConfigCheckpoints from './listConfigCheckpoints.js';
|
|
34
|
+
import listPlugins from './listPlugins.js';
|
|
35
|
+
import listTypes from './listTypes.js';
|
|
36
|
+
import loadState from './loadState.js';
|
|
37
|
+
import revertConfigCheckpoint from './revertConfigCheckpoint.js';
|
|
38
|
+
import runRequest from './runRequest.js';
|
|
39
|
+
import snapshotState from './snapshotState.js';
|
|
40
|
+
import { listStateCheckpoints } from './checkpointStore.js';
|
|
41
|
+
import createLogger from '../server/log/createLogger.js';
|
|
42
|
+
import scaffoldPage from './scaffoldPage.js';
|
|
43
|
+
import screenshotPage from './screenshotPage.js';
|
|
44
|
+
import searchDocs from './searchDocs.js';
|
|
45
|
+
|
|
46
|
+
const logger = createLogger({ server: 'lowdefy-dev-mcp' });
|
|
47
|
+
|
|
48
|
+
const INSTRUCTIONS = `Lowdefy documentation and feedback server for this project. Lowdefy apps are YAML config composing blocks (UI), operators (logic), actions (event handlers), and connections/requests (data).
|
|
49
|
+
|
|
50
|
+
Discovery workflow: start with lowdefy_overview. Use lowdefy_list_types with a kind to discover ALL installed blocks/operators/actions/connections/requests — never guess type names. Then lowdefy_get_schema and lowdefy_get_examples for the exact contract of a type, and lowdefy_get_doc / lowdefy_search_docs for concept documentation. lowdefy_list_plugins and lowdefy_get_plugin_doc cover this project's local plugin packages.
|
|
51
|
+
|
|
52
|
+
Feedback loop: after EVERY config edit, call lowdefy_build_status — the dev server rebuilds on file change and this returns the current build errors/warnings (with source file locations) plus recent browser runtime errors. Fix what it reports, then confirm the page builds with lowdefy_get_page_config, and visually verify with lowdefy_screenshot_page. Use lowdefy_find_config to locate where any id (page, block, request) is defined. lowdefy_scaffold_page creates a canonical new page file. Use lowdefy_app_map first to understand an existing app.
|
|
53
|
+
|
|
54
|
+
Live state: lowdefy_inspect_state reads the ACTUAL state, request results, and event log of a running page — when the developer has the page open in their browser it reads THEIR live tab (ask them to interact, then inspect), otherwise it runs the page headless. lowdefy_eval_operator evaluates any operator expression against that live state — use it to debug _state/_request bindings. lowdefy_run_request executes a request with a test payload to verify data shape (read-only unless the app opts into writes).
|
|
55
|
+
|
|
56
|
+
Role-gated pages: the headless renderer signs in as a roleless user, so a page or request gated on a role renders empty or refused. Pass user to lowdefy_screenshot_page, lowdefy_inspect_state, lowdefy_eval_operator or lowdefy_load_state to act as a specific caller — e.g. user {"roles":["admin"]} — and vary it per call to compare what different roles see.
|
|
57
|
+
|
|
58
|
+
Safety: lowdefy_checkpoint snapshots the config files before risky multi-file changes; lowdefy_revert_checkpoint restores them.
|
|
59
|
+
|
|
60
|
+
Visual feedback: developers can press Cmd/Ctrl+/ in the running app to point at elements, draw, and copy annotated feedback to their clipboard, then paste it to you. Pasted annotation blocks start with "Feedback:" and carry the blockId, the resolved config file:line, drawn shapes, and usually an "Annotated screenshot:" file path — READ that image to see exactly what the developer drew. Treat them as precise UI feedback and use lowdefy_inspect_state for the page's live state.
|
|
61
|
+
|
|
62
|
+
State checkpoints (testing): lowdefy_snapshot_state captures a page's live state AND its request/api responses into .lowdefy/state-checkpoints/<name>/ (one file per part; gitignored — checkpoints contain user/session data). lowdefy_load_state puts the app back into that state: headless for your own verification, or registry-only which returns a ?_checkpoint URL the developer can open to manually test the app in that exact state (recorded request data is served automatically). lowdefy_checkpoint_to_mocks converts a checkpoint into e2e mocks.yaml fixtures — use it when asked to write e2e tests.`;
|
|
63
|
+
|
|
64
|
+
function textResult(value) {
|
|
65
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
66
|
+
return { content: [{ type: 'text', text }] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function notFoundResult(message) {
|
|
70
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Shared by every tool that renders a page headless, so one call can act as an
|
|
74
|
+
// admin and the next as a plain member — each headless call gets its own browser
|
|
75
|
+
// context, so they never share an identity.
|
|
76
|
+
const userSchema = z
|
|
77
|
+
.object({})
|
|
78
|
+
.passthrough()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(
|
|
81
|
+
'Act as this caller instead of the default roleless headless user, e.g. {"roles":["user-admin"]} to render a role-gated page. Merged over the default, so include email/profile/attributes fields too if the page reads them — no auth engine runs for an injected caller, so nothing derives them. Headless only: it is never applied to a page the developer opens in their own browser, so combining it with source "tab" or load_state mode "registry-only" is an error rather than a silently dropped role.'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
function createDocsMcpServer({ origin, honoContext } = {}) {
|
|
85
|
+
const server = new McpServer(
|
|
86
|
+
{ name: 'lowdefy-docs', version: '1.0.0' },
|
|
87
|
+
{ instructions: INSTRUCTIONS }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Debug-log every tool call (name + args, never the response) so agent
|
|
91
|
+
// activity is visible in the dev terminal with --log-level=debug. Wrapping
|
|
92
|
+
// registerTool here covers all tools without touching each registration.
|
|
93
|
+
const registerTool = server.registerTool.bind(server);
|
|
94
|
+
server.registerTool = (name, definition, handler) =>
|
|
95
|
+
registerTool(name, definition, async (args, extra) => {
|
|
96
|
+
try {
|
|
97
|
+
logger.debug({ event: 'mcp_tool_call', tool: name, args }, `MCP tool call: ${name}`);
|
|
98
|
+
} catch {
|
|
99
|
+
// Logging must never break a tool call.
|
|
100
|
+
}
|
|
101
|
+
return handler(args, extra);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
server.registerTool(
|
|
105
|
+
'lowdefy_inspect_state',
|
|
106
|
+
{
|
|
107
|
+
description:
|
|
108
|
+
"Read the LIVE state of a running page: state, request results, event log (recent actions fired), global, user, input, and urlQuery. If the developer has the page open in a browser it reads their actual tab (ask them to interact first, then inspect); otherwise it runs the page headless. Use this to see what the app's data model really looks like.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
pageId: z.string().describe('The page id to inspect.'),
|
|
111
|
+
source: z
|
|
112
|
+
.enum(['tab', 'headless'])
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Force a source. Default: live tab if connected, else headless.'),
|
|
115
|
+
user: userSchema,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
async ({ pageId, source, user }) => {
|
|
119
|
+
const result = await inspectState({ origin, pageId, source, user });
|
|
120
|
+
if (result.error) {
|
|
121
|
+
return notFoundResult(result.error);
|
|
122
|
+
}
|
|
123
|
+
return textResult(result);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
server.registerTool(
|
|
128
|
+
'lowdefy_eval_operator',
|
|
129
|
+
{
|
|
130
|
+
description:
|
|
131
|
+
'Evaluate a Lowdefy operator expression against the live state of a running page — a REPL for config. Pass the operator object in the "expression" argument — any JSON value, e.g. {"_state": "customer.name"} or {"_if": {...}}. Evaluates in the real browser runtime (live tab if connected, else headless).',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
pageId: z.string().describe('The page id whose context to evaluate against.'),
|
|
134
|
+
expression: z
|
|
135
|
+
.any()
|
|
136
|
+
.describe('The operator expression — any JSON value, e.g. {"_state": "key"}.'),
|
|
137
|
+
source: z.enum(['tab', 'headless']).optional(),
|
|
138
|
+
user: userSchema,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
async ({ pageId, expression, source, user }) => {
|
|
142
|
+
const result = await evalOperator({ origin, pageId, expression, source, user });
|
|
143
|
+
if (result.error) {
|
|
144
|
+
return notFoundResult(result.error);
|
|
145
|
+
}
|
|
146
|
+
return textResult(result);
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
server.registerTool(
|
|
151
|
+
'lowdefy_run_request',
|
|
152
|
+
{
|
|
153
|
+
description:
|
|
154
|
+
'Execute a request in dev with a test payload to verify the data shape a page receives. Read-only request types always run; write requests are refused unless the app opts in (cli.agentTools.allowWriteRequests in lowdefy.yaml).',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
pageId: z.string().describe('The page the request is defined on.'),
|
|
157
|
+
requestId: z.string().describe('The request id.'),
|
|
158
|
+
payload: z.record(z.any()).optional().describe('Test payload for _payload operators.'),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async ({ pageId, requestId, payload }) =>
|
|
162
|
+
textResult(await runRequest({ pageId, requestId, payload, honoContext }))
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
server.registerTool(
|
|
166
|
+
'lowdefy_app_map',
|
|
167
|
+
{
|
|
168
|
+
description:
|
|
169
|
+
'The whole-app graph in one call: every page (with its source file and, when built, block/request summaries), menus, connections, api endpoints, agents, and websockets. Call this first when working in an existing app.',
|
|
170
|
+
inputSchema: {},
|
|
171
|
+
},
|
|
172
|
+
() => textResult(getAppMap())
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.registerTool(
|
|
176
|
+
'lowdefy_snapshot_state',
|
|
177
|
+
{
|
|
178
|
+
description:
|
|
179
|
+
"Capture the live state AND recorded request/api responses of a running page into a checkpoint folder (.lowdefy/state-checkpoints/<name>/, one file per part; gitignored — checkpoints contain user/session data). Snapshot the developer's open tab after they reproduce a scenario, or a headless run. Use for building test fixtures and reproducible app states.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
pageId: z.string().describe('The page to snapshot.'),
|
|
182
|
+
name: z.string().describe('Checkpoint name (letters, numbers, - and _).'),
|
|
183
|
+
notes: z.string().optional().describe('What this checkpoint captures.'),
|
|
184
|
+
source: z.enum(['tab', 'headless']).optional(),
|
|
185
|
+
overwrite: z.boolean().optional(),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
async ({ pageId, name, notes, source, overwrite }) => {
|
|
189
|
+
const result = await snapshotState({ origin, pageId, name, notes, source, overwrite });
|
|
190
|
+
if (result.error) {
|
|
191
|
+
return notFoundResult(result.error);
|
|
192
|
+
}
|
|
193
|
+
return textResult(result);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
server.registerTool(
|
|
198
|
+
'lowdefy_load_state',
|
|
199
|
+
{
|
|
200
|
+
description:
|
|
201
|
+
"Put the app back into a saved state checkpoint. mode 'headless' (default) verifies the restored state itself; mode 'registry-only' loads the recorded request data into the dev server and returns a ?_checkpoint URL the developer can open to manually test the app in that exact state.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
name: z.string().describe('The checkpoint name.'),
|
|
204
|
+
mode: z.enum(['headless', 'registry-only']).optional(),
|
|
205
|
+
user: userSchema,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
async ({ name, mode, user }) => {
|
|
209
|
+
const result = await loadState({ origin, name, mode, user });
|
|
210
|
+
if (result.error) {
|
|
211
|
+
return notFoundResult(result.error);
|
|
212
|
+
}
|
|
213
|
+
return textResult(result);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
server.registerTool(
|
|
218
|
+
'lowdefy_list_state_checkpoints',
|
|
219
|
+
{
|
|
220
|
+
description: 'List saved state checkpoints (name, page, captured time, notes).',
|
|
221
|
+
inputSchema: {},
|
|
222
|
+
},
|
|
223
|
+
() => textResult(listStateCheckpoints())
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
server.registerTool(
|
|
227
|
+
'lowdefy_checkpoint_to_mocks',
|
|
228
|
+
{
|
|
229
|
+
description:
|
|
230
|
+
'Convert a state checkpoint into @lowdefy/e2e-utils mocks.yaml fixtures (requests/api entries plus rendered yaml). Use when writing e2e tests from a captured scenario.',
|
|
231
|
+
inputSchema: {
|
|
232
|
+
name: z.string().describe('The checkpoint name.'),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
({ name }) => {
|
|
236
|
+
const result = checkpointToMocks({ name });
|
|
237
|
+
if (result.error) {
|
|
238
|
+
return notFoundResult(result.error);
|
|
239
|
+
}
|
|
240
|
+
return textResult(result);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
server.registerTool(
|
|
245
|
+
'lowdefy_checkpoint',
|
|
246
|
+
{
|
|
247
|
+
description:
|
|
248
|
+
'Snapshot all config files before risky changes. Returns a checkpoint id for lowdefy_revert_checkpoint. Use before multi-file edits so you can restore instantly.',
|
|
249
|
+
inputSchema: {
|
|
250
|
+
label: z.string().describe('Short label for the checkpoint, e.g. "before-refactor".'),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
({ label }) => textResult(createConfigCheckpoint({ label }))
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
server.registerTool(
|
|
257
|
+
'lowdefy_revert_checkpoint',
|
|
258
|
+
{
|
|
259
|
+
description:
|
|
260
|
+
'Restore config files from a checkpoint made with lowdefy_checkpoint (restores changed files and removes files added since). Omit id to list available checkpoints.',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
id: z.string().optional().describe('Checkpoint id. Omit to list checkpoints.'),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
({ id }) => {
|
|
266
|
+
if (!id) {
|
|
267
|
+
return textResult(listConfigCheckpoints());
|
|
268
|
+
}
|
|
269
|
+
return textResult(revertConfigCheckpoint({ id }));
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
server.registerTool(
|
|
274
|
+
'lowdefy_build_status',
|
|
275
|
+
{
|
|
276
|
+
description:
|
|
277
|
+
'Call after every config edit. Returns the current build status: errors and warnings from the last build (with source file locations), plus recent browser runtime errors. The dev server rebuilds automatically on file change — edit, then call this to see what broke.',
|
|
278
|
+
inputSchema: {},
|
|
279
|
+
},
|
|
280
|
+
() => textResult(getBuildStatus())
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
server.registerTool(
|
|
284
|
+
'lowdefy_get_page_config',
|
|
285
|
+
{
|
|
286
|
+
description:
|
|
287
|
+
'Get the fully built config for a page, or its structured build errors if the page fails to build. Use to verify a page after editing it.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
pageId: z.string().describe('The page id.'),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
async ({ pageId }) => {
|
|
293
|
+
const result = await getPageConfig({ pageId });
|
|
294
|
+
if (result === null) {
|
|
295
|
+
return notFoundResult(
|
|
296
|
+
`Page "${pageId}" not found. Use lowdefy_overview or check pageRegistry for valid page ids.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return textResult(result);
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
server.registerTool(
|
|
304
|
+
'lowdefy_find_config',
|
|
305
|
+
{
|
|
306
|
+
description:
|
|
307
|
+
'Find where a config entity is defined: pass a page, block, or request id and get the source yaml file (and line where available). For block/request ids also pass the owning pageId so the page is built first.',
|
|
308
|
+
inputSchema: {
|
|
309
|
+
id: z.string().describe('The id to find, e.g. a pageId, blockId, or requestId.'),
|
|
310
|
+
pageId: z
|
|
311
|
+
.string()
|
|
312
|
+
.optional()
|
|
313
|
+
.describe(
|
|
314
|
+
'The page the id belongs to — required for block/request ids on pages not yet built.'
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
async ({ id, pageId }) => textResult(await findConfig({ id, pageId }))
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
server.registerTool(
|
|
322
|
+
'lowdefy_screenshot_page',
|
|
323
|
+
{
|
|
324
|
+
description:
|
|
325
|
+
'Screenshot a page of the running dev server (headless Chromium) to visually verify layout and rendering. Returns a PNG image.',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
pageId: z.string().describe('The page id to screenshot.'),
|
|
328
|
+
fullPage: z.boolean().optional().describe('Capture the full scrollable page.'),
|
|
329
|
+
clip: z
|
|
330
|
+
.object({
|
|
331
|
+
x: z.number(),
|
|
332
|
+
y: z.number(),
|
|
333
|
+
width: z.number(),
|
|
334
|
+
height: z.number(),
|
|
335
|
+
})
|
|
336
|
+
.optional()
|
|
337
|
+
.describe(
|
|
338
|
+
"Crop to a viewport-relative region — pass an annotation's geometry from feedback."
|
|
339
|
+
),
|
|
340
|
+
scrollX: z.number().optional().describe('Scroll offset the clip was recorded at.'),
|
|
341
|
+
scrollY: z.number().optional().describe('Scroll offset the clip was recorded at.'),
|
|
342
|
+
user: userSchema,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
async ({ pageId, fullPage, clip, scrollX, scrollY, user }) => {
|
|
346
|
+
if (!origin) {
|
|
347
|
+
return notFoundResult('Screenshot unavailable: server origin unknown for this transport.');
|
|
348
|
+
}
|
|
349
|
+
const result = await screenshotPage({
|
|
350
|
+
origin,
|
|
351
|
+
pageId,
|
|
352
|
+
fullPage,
|
|
353
|
+
clip,
|
|
354
|
+
scrollX,
|
|
355
|
+
scrollY,
|
|
356
|
+
user,
|
|
357
|
+
});
|
|
358
|
+
if (result.error) {
|
|
359
|
+
return notFoundResult(result.error);
|
|
360
|
+
}
|
|
361
|
+
return { content: [{ type: 'image', data: result.data, mimeType: result.mimeType }] };
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
server.registerTool(
|
|
366
|
+
'lowdefy_scaffold_page',
|
|
367
|
+
{
|
|
368
|
+
description:
|
|
369
|
+
'Create a new page yaml file with a canonical minimal structure. Refuses if the page already exists. Returns the created file path and the lowdefy.yaml registration step you must do next.',
|
|
370
|
+
inputSchema: {
|
|
371
|
+
pageId: z.string().describe('The new page id (letters, numbers, - and _).'),
|
|
372
|
+
title: z.string().optional().describe('Page title. Defaults to the pageId.'),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
({ pageId, title }) => {
|
|
376
|
+
const result = scaffoldPage({ pageId, title });
|
|
377
|
+
if (result.error) {
|
|
378
|
+
return notFoundResult(result.error);
|
|
379
|
+
}
|
|
380
|
+
return textResult(result);
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
server.registerTool(
|
|
385
|
+
'lowdefy_overview',
|
|
386
|
+
{
|
|
387
|
+
description:
|
|
388
|
+
'Start here. Overview of everything this Lowdefy project has available: counts of blocks/operators/actions/connections/requests, installed plugins, doc sections, and which tool to use next.',
|
|
389
|
+
inputSchema: {},
|
|
390
|
+
},
|
|
391
|
+
() => textResult(getOverview())
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
server.registerTool(
|
|
395
|
+
'lowdefy_list_types',
|
|
396
|
+
{
|
|
397
|
+
description:
|
|
398
|
+
'List ALL available types of a kind — every block, operator, action, connection, or request type installed in this project (core and local plugins), whether used yet or not. Call this before writing any config to get exact type names.',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
kind: z
|
|
401
|
+
.enum([
|
|
402
|
+
'blocks',
|
|
403
|
+
'operators',
|
|
404
|
+
'actions',
|
|
405
|
+
'connections',
|
|
406
|
+
'requests',
|
|
407
|
+
'agents',
|
|
408
|
+
'notifications',
|
|
409
|
+
'websockets',
|
|
410
|
+
])
|
|
411
|
+
.describe('The kind of types to list.'),
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
({ kind }) => textResult(listTypes({ kind }))
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
server.registerTool(
|
|
418
|
+
'lowdefy_list_plugins',
|
|
419
|
+
{
|
|
420
|
+
description:
|
|
421
|
+
"List installed plugin packages (including this project's local custom plugins) and the type names each provides.",
|
|
422
|
+
inputSchema: {},
|
|
423
|
+
},
|
|
424
|
+
() => textResult(listPlugins())
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
server.registerTool(
|
|
428
|
+
'lowdefy_get_schema',
|
|
429
|
+
{
|
|
430
|
+
description:
|
|
431
|
+
'Get the JSON Schema for a specific type: all properties, events, and their descriptions. Use the exact type name from lowdefy_list_types.',
|
|
432
|
+
inputSchema: {
|
|
433
|
+
kind: z
|
|
434
|
+
.enum(['blocks', 'operators', 'actions', 'connections', 'requests'])
|
|
435
|
+
.describe('The kind of the type.'),
|
|
436
|
+
type: z.string().describe('The exact type name, e.g. "Button", "_get", "MongoDBFind".'),
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
({ kind, type }) => {
|
|
440
|
+
const schema = getSchema({ kind, type });
|
|
441
|
+
if (schema === null) {
|
|
442
|
+
return notFoundResult(
|
|
443
|
+
`No schema found for ${kind} type "${type}". Use lowdefy_list_types to see available types.`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return textResult(schema);
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
server.registerTool(
|
|
451
|
+
'lowdefy_get_examples',
|
|
452
|
+
{
|
|
453
|
+
description:
|
|
454
|
+
'Get real YAML usage examples for a block type (gallery and example configs shipped with the plugin).',
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: z.string().describe('The exact block type name, e.g. "Button".'),
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
({ type }) => {
|
|
460
|
+
const examples = getExamples({ type });
|
|
461
|
+
if (examples === null) {
|
|
462
|
+
return notFoundResult(
|
|
463
|
+
`No examples shipped for block type "${type}". Use lowdefy_get_schema for its contract, or lowdefy_get_doc for its documentation page.`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return textResult(examples);
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
server.registerTool(
|
|
471
|
+
'lowdefy_get_doc',
|
|
472
|
+
{
|
|
473
|
+
description:
|
|
474
|
+
'Get a core Lowdefy documentation page as markdown. Look up by slug (e.g. "concepts/lowdefy-schema", "operators/_get") or by kind + type name. Key concept slugs: concepts/lowdefy-schema, concepts/blocks, concepts/events-and-actions, concepts/connections-and-requests, concepts/operators, concepts/page-and-app-state.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
slug: z.string().optional().describe('Doc slug, e.g. "operators/_get".'),
|
|
477
|
+
kind: z
|
|
478
|
+
.enum(['block', 'operator', 'action', 'connection'])
|
|
479
|
+
.optional()
|
|
480
|
+
.describe('Kind of the type to find the doc for.'),
|
|
481
|
+
type: z.string().optional().describe('Type name to find the doc for, e.g. "_get".'),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
({ slug, kind, type }) => {
|
|
485
|
+
const doc = getCoreDoc({ slug, kind, type });
|
|
486
|
+
if (doc === null) {
|
|
487
|
+
return notFoundResult(
|
|
488
|
+
`No doc found${slug ? ` for slug "${slug}"` : ''}${
|
|
489
|
+
type ? ` for type "${type}"` : ''
|
|
490
|
+
}. Use lowdefy_search_docs to find the right slug.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return textResult(doc.markdown);
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
server.registerTool(
|
|
498
|
+
'lowdefy_search_docs',
|
|
499
|
+
{
|
|
500
|
+
description: 'Search the core Lowdefy docs by keyword. Returns matching slugs with snippets.',
|
|
501
|
+
inputSchema: {
|
|
502
|
+
query: z.string().describe('Search keywords.'),
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
({ query }) => textResult(searchDocs({ query }))
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
server.registerTool(
|
|
509
|
+
'lowdefy_get_plugin_doc',
|
|
510
|
+
{
|
|
511
|
+
description:
|
|
512
|
+
"Get markdown documentation shipped inside an installed plugin package (README, guides). Useful for this project's local custom plugins.",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
package: z.string().describe('The package name, e.g. "@lowdefy/blocks-antd".'),
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
({ package: packageName }) => {
|
|
518
|
+
const doc = getPluginDoc({ packageName });
|
|
519
|
+
if (doc === null) {
|
|
520
|
+
return notFoundResult(
|
|
521
|
+
`Package "${packageName}" ships no markdown docs. Use lowdefy_list_types and lowdefy_get_schema for its types.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return textResult(doc.markdown);
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
return server;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export default createDocsMcpServer;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
19
|
+
|
|
20
|
+
import setupTestFixtures from './setupTestFixtures.mjs';
|
|
21
|
+
|
|
22
|
+
// The docs service reads build artifacts from process.cwd() — point it at the
|
|
23
|
+
// fixture server before the modules are imported.
|
|
24
|
+
const fixtureDir = setupTestFixtures();
|
|
25
|
+
process.chdir(fixtureDir);
|
|
26
|
+
|
|
27
|
+
const { default: createDocsMcpServer } = await import('./createDocsMcpServer.js');
|
|
28
|
+
|
|
29
|
+
const EXPECTED_TOOLS = [
|
|
30
|
+
'lowdefy_overview',
|
|
31
|
+
'lowdefy_list_types',
|
|
32
|
+
'lowdefy_list_plugins',
|
|
33
|
+
'lowdefy_get_schema',
|
|
34
|
+
'lowdefy_get_examples',
|
|
35
|
+
'lowdefy_get_doc',
|
|
36
|
+
'lowdefy_search_docs',
|
|
37
|
+
'lowdefy_get_plugin_doc',
|
|
38
|
+
'lowdefy_build_status',
|
|
39
|
+
'lowdefy_get_page_config',
|
|
40
|
+
'lowdefy_find_config',
|
|
41
|
+
'lowdefy_screenshot_page',
|
|
42
|
+
'lowdefy_scaffold_page',
|
|
43
|
+
'lowdefy_inspect_state',
|
|
44
|
+
'lowdefy_eval_operator',
|
|
45
|
+
'lowdefy_run_request',
|
|
46
|
+
'lowdefy_app_map',
|
|
47
|
+
'lowdefy_checkpoint',
|
|
48
|
+
'lowdefy_revert_checkpoint',
|
|
49
|
+
'lowdefy_snapshot_state',
|
|
50
|
+
'lowdefy_load_state',
|
|
51
|
+
'lowdefy_list_state_checkpoints',
|
|
52
|
+
'lowdefy_checkpoint_to_mocks',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
async function connectClient() {
|
|
56
|
+
const server = createDocsMcpServer({ origin: 'http://localhost:3000' });
|
|
57
|
+
const client = new Client({ name: 'test-client', version: '1.0.0' });
|
|
58
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
59
|
+
await server.connect(serverTransport);
|
|
60
|
+
await client.connect(clientTransport);
|
|
61
|
+
return client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test('MCP server exposes instructions teaching the feedback loop', async () => {
|
|
65
|
+
const client = await connectClient();
|
|
66
|
+
const instructions = client.getInstructions();
|
|
67
|
+
expect(instructions).toContain('lowdefy_build_status');
|
|
68
|
+
expect(instructions).toContain('never guess type names');
|
|
69
|
+
await client.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('MCP tools/list returns all lowdefy tools', async () => {
|
|
73
|
+
const client = await connectClient();
|
|
74
|
+
const { tools } = await client.listTools();
|
|
75
|
+
const names = tools.map((tool) => tool.name).sort();
|
|
76
|
+
expect(names).toEqual([...EXPECTED_TOOLS].sort());
|
|
77
|
+
await client.close();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('MCP tools that render a page headless advertise an optional user parameter', async () => {
|
|
81
|
+
const client = await connectClient();
|
|
82
|
+
const { tools } = await client.listTools();
|
|
83
|
+
|
|
84
|
+
[
|
|
85
|
+
'lowdefy_screenshot_page',
|
|
86
|
+
'lowdefy_inspect_state',
|
|
87
|
+
'lowdefy_eval_operator',
|
|
88
|
+
'lowdefy_load_state',
|
|
89
|
+
].forEach((name) => {
|
|
90
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
91
|
+
expect(tool.inputSchema.properties.user).toBeDefined();
|
|
92
|
+
expect(tool.inputSchema.required ?? []).not.toContain('user');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await client.close();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('MCP tools/call lowdefy_get_schema returns a block schema', async () => {
|
|
99
|
+
const client = await connectClient();
|
|
100
|
+
const result = await client.callTool({
|
|
101
|
+
name: 'lowdefy_get_schema',
|
|
102
|
+
arguments: { kind: 'blocks', type: 'Button' },
|
|
103
|
+
});
|
|
104
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
105
|
+
expect(parsed.schema.properties.title).toBeDefined();
|
|
106
|
+
await client.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('MCP tools/call lowdefy_build_status returns build status with client errors', async () => {
|
|
110
|
+
const client = await connectClient();
|
|
111
|
+
const result = await client.callTool({ name: 'lowdefy_build_status', arguments: {} });
|
|
112
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
113
|
+
expect(parsed.build.status).toEqual('ok');
|
|
114
|
+
expect(Array.isArray(parsed.clientErrors)).toBe(true);
|
|
115
|
+
await client.close();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('MCP tools/call lowdefy_find_config locates a block by id', async () => {
|
|
119
|
+
const client = await connectClient();
|
|
120
|
+
const result = await client.callTool({
|
|
121
|
+
name: 'lowdefy_find_config',
|
|
122
|
+
arguments: { id: 'home' },
|
|
123
|
+
});
|
|
124
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
125
|
+
expect(parsed.kind).toEqual('page');
|
|
126
|
+
expect(parsed.pageId).toEqual('home');
|
|
127
|
+
expect(parsed.file).toEqual('pages/home.yaml');
|
|
128
|
+
expect(parsed.matches[0].location.source).toContain('pages/home.yaml');
|
|
129
|
+
await client.close();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('MCP tools/call with an unknown type returns isError with guidance', async () => {
|
|
133
|
+
const client = await connectClient();
|
|
134
|
+
const result = await client.callTool({
|
|
135
|
+
name: 'lowdefy_get_schema',
|
|
136
|
+
arguments: { kind: 'blocks', type: 'NoSuchBlock' },
|
|
137
|
+
});
|
|
138
|
+
expect(result.isError).toBe(true);
|
|
139
|
+
expect(result.content[0].text).toContain('lowdefy_list_types');
|
|
140
|
+
await client.close();
|
|
141
|
+
});
|