@dypai-ai/mcp 1.2.1 → 1.2.3
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/package.json +1 -1
- package/src/tools/proxy.js +9 -0
- package/src/tools/scaffold.js +13 -1
- package/src/tools/sync/path-resolver.js +99 -0
- package/src/tools/sync/push.js +37 -4
package/package.json
CHANGED
package/src/tools/proxy.js
CHANGED
|
@@ -129,6 +129,15 @@ export async function proxyToolCall(toolName, args) {
|
|
|
129
129
|
if (typeof parsed === "string" && /^(Error:|Input validation error:|You don't have)/i.test(parsed)) {
|
|
130
130
|
throw new Error(parsed)
|
|
131
131
|
}
|
|
132
|
+
// Other remote errors come as a JSON object with success:false but no isError
|
|
133
|
+
// flag. The push pipeline was treating these as successes — promote them to
|
|
134
|
+
// throws here so the caller's try/catch sees them.
|
|
135
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
136
|
+
if (parsed.success === false || parsed.ok === false) {
|
|
137
|
+
const msg = parsed.error || parsed.message || parsed.detail || JSON.stringify(parsed).slice(0, 300)
|
|
138
|
+
throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
132
141
|
return parsed
|
|
133
142
|
}
|
|
134
143
|
return response.result
|
package/src/tools/scaffold.js
CHANGED
|
@@ -108,7 +108,19 @@ Or use "blank" for an empty starter project.`,
|
|
|
108
108
|
template: template || "blank",
|
|
109
109
|
engine_url: engineUrl,
|
|
110
110
|
sdk_client: "src/lib/dypai.ts",
|
|
111
|
-
|
|
111
|
+
// Critical: dypai_pull without an absolute out_dir can land in $HOME
|
|
112
|
+
// when the MCP process cwd isn't the workspace. Tell the agent exactly
|
|
113
|
+
// what to pass next so the backend materializes INSIDE this project.
|
|
114
|
+
next_step: {
|
|
115
|
+
action: "materialize_backend",
|
|
116
|
+
tool: "dypai_pull",
|
|
117
|
+
args: {
|
|
118
|
+
project_id,
|
|
119
|
+
out_dir: join(directory, "dypai"),
|
|
120
|
+
},
|
|
121
|
+
why: "Use the ABSOLUTE dypai/ path inside the project you just created. A relative './dypai' can resolve to the wrong folder (e.g. $HOME) when the MCP isn't running from your workspace.",
|
|
122
|
+
},
|
|
123
|
+
message: `Project created at ${directory} with ${created} files.${installed ? " Dependencies installed." : " Run 'npm install' to install dependencies."} SDK client ready at src/lib/dypai.ts — import { dypai } from './lib/dypai' to use. Next: call dypai_pull with out_dir="${join(directory, "dypai")}" to materialize the backend inside this project.`,
|
|
112
124
|
}
|
|
113
125
|
},
|
|
114
126
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-aware path resolution for dypai/ folder.
|
|
3
|
+
*
|
|
4
|
+
* The MCP often runs from $HOME (default cwd when an IDE spawns it over stdio).
|
|
5
|
+
* A naive `resolvePath(cwd, "./dypai")` lands in `~/dypai/` — silently writing
|
|
6
|
+
* files where the user will never find them, OR silently reading from an empty
|
|
7
|
+
* folder so push reports "no_changes" and the agent thinks everything is fine.
|
|
8
|
+
*
|
|
9
|
+
* This helper centralizes the resolution + suspicious-path detection so pull
|
|
10
|
+
* AND push share the exact same logic. Hard-stop is the caller's responsibility.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "fs"
|
|
14
|
+
import { isAbsolute, dirname, join, resolve as resolvePath, sep, delimiter } from "path"
|
|
15
|
+
import { homedir } from "os"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a (possibly relative) dypai/ path. Walks several signals in order:
|
|
19
|
+
* 1. absolute → use as-is
|
|
20
|
+
* 2. env vars set by IDEs (CLAUDE_PROJECT_DIR, WORKSPACE_FOLDER_PATHS, etc.)
|
|
21
|
+
* 3. walk UP from cwd looking for project markers (.git, package.json, dypai/)
|
|
22
|
+
* 4. fall back to cwd (often $HOME — flagged as suspicious by the warning fn)
|
|
23
|
+
*
|
|
24
|
+
* Returns { path, source } so the caller can render a helpful trace.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveOutDir(outDir) {
|
|
27
|
+
if (isAbsolute(outDir)) return { path: outDir, source: "absolute" }
|
|
28
|
+
|
|
29
|
+
const envCandidates = [
|
|
30
|
+
["CLAUDE_PROJECT_DIR", process.env.CLAUDE_PROJECT_DIR],
|
|
31
|
+
["DYPAI_WORKSPACE_ROOT", process.env.DYPAI_WORKSPACE_ROOT],
|
|
32
|
+
["WORKSPACE_FOLDER_PATHS", process.env.WORKSPACE_FOLDER_PATHS?.split(delimiter)[0]],
|
|
33
|
+
["PROJECT_ROOT", process.env.PROJECT_ROOT],
|
|
34
|
+
]
|
|
35
|
+
for (const [name, val] of envCandidates) {
|
|
36
|
+
if (val && isAbsolute(val)) return { path: resolvePath(val, outDir), source: `env:${name}` }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cursor = process.cwd()
|
|
40
|
+
for (let i = 0; i < 6; i++) {
|
|
41
|
+
if (existsSync(join(cursor, ".git")) ||
|
|
42
|
+
existsSync(join(cursor, "package.json")) ||
|
|
43
|
+
existsSync(join(cursor, "dypai"))) {
|
|
44
|
+
return { path: resolvePath(cursor, outDir), source: "project_marker" }
|
|
45
|
+
}
|
|
46
|
+
const parent = dirname(cursor)
|
|
47
|
+
if (parent === cursor) break
|
|
48
|
+
cursor = parent
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { path: resolvePath(process.cwd(), outDir), source: "cwd_fallback" }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return a warning string when the resolved path looks suspicious.
|
|
56
|
+
* Specifically: cwd_fallback that landed inside $HOME at depth ≤ 2.
|
|
57
|
+
* Returns null when the path looks fine.
|
|
58
|
+
*/
|
|
59
|
+
export function suspiciousPathWarning(resolvedPath, source) {
|
|
60
|
+
if (source === "absolute") return null
|
|
61
|
+
const home = homedir()
|
|
62
|
+
if (source === "cwd_fallback" && resolvedPath.startsWith(home + sep)) {
|
|
63
|
+
const rel = resolvedPath.slice(home.length + 1)
|
|
64
|
+
if (!rel.includes(sep) || rel.split(sep).length <= 2) {
|
|
65
|
+
return (
|
|
66
|
+
`Output landed at ${resolvedPath}. The MCP process cwd is your home dir, not your project. ` +
|
|
67
|
+
`Re-run with an ABSOLUTE path (e.g. ${home}/path/to/your-project/dypai) ` +
|
|
68
|
+
`or set the CLAUDE_PROJECT_DIR env var on the MCP entry.`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convenience wrapper: resolve + suspicious check + structured error builder.
|
|
77
|
+
* Returns { ok: true, path, source } on success, or { ok: false, error } when
|
|
78
|
+
* the resolved path is suspicious. Pull/push pass the error response straight back.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveAndGuard(outDir, { project_id, tool, arg_name = "out_dir" } = {}) {
|
|
81
|
+
const { path, source } = resolveOutDir(outDir)
|
|
82
|
+
const warning = suspiciousPathWarning(path, source)
|
|
83
|
+
if (!warning) return { ok: true, path, source }
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: {
|
|
88
|
+
success: false,
|
|
89
|
+
error: "Resolved path looks wrong — aborting before reading/writing any files.",
|
|
90
|
+
resolved_to: path,
|
|
91
|
+
resolved_via: source,
|
|
92
|
+
explanation: warning,
|
|
93
|
+
fix:
|
|
94
|
+
`Call ${tool || "this tool"} again with an ABSOLUTE ${arg_name} pointing inside your project, e.g.\n` +
|
|
95
|
+
` ${tool || "<tool>"}({ ${project_id ? `project_id: "${project_id}", ` : ""}${arg_name}: "/absolute/path/to/your-project/dypai" })\n` +
|
|
96
|
+
`If you just ran download_template, reuse the "directory" it returned and append "/dypai".`,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/tools/sync/push.js
CHANGED
|
@@ -61,26 +61,52 @@ function endpointPayload(row) {
|
|
|
61
61
|
return p
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Treat a remote tool response as a definite success only when it has at
|
|
66
|
+
* least one of the markers we expect from a real mutation. Anything else
|
|
67
|
+
* (empty object, error string masquerading as data, missing id) becomes
|
|
68
|
+
* an error so the push doesn't lie to the agent.
|
|
69
|
+
*/
|
|
70
|
+
function assertMutationOK(result, op, name) {
|
|
71
|
+
if (!result || typeof result !== "object") {
|
|
72
|
+
throw new Error(`${op} ${name}: empty response from remote (got ${JSON.stringify(result).slice(0, 120)})`)
|
|
73
|
+
}
|
|
74
|
+
if (result.error) {
|
|
75
|
+
throw new Error(`${op} ${name}: ${typeof result.error === "string" ? result.error : JSON.stringify(result.error)}`)
|
|
76
|
+
}
|
|
77
|
+
// Successful create returns { id, name } or { ok: true }; update/delete return
|
|
78
|
+
// { message } or { ok: true }. If none of these markers are present the call
|
|
79
|
+
// probably failed silently.
|
|
80
|
+
const hasMarker = result.id || result.ok === true || result.message || result.endpoint_id || result.success === true
|
|
81
|
+
if (!hasMarker) {
|
|
82
|
+
throw new Error(`${op} ${name}: response missing success markers — ${JSON.stringify(result).slice(0, 200)}`)
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
async function applyCreate(canonicalRow, projectId) {
|
|
65
|
-
|
|
88
|
+
const res = await proxyToolCall("create_endpoint", {
|
|
66
89
|
...(projectId ? { project_id: projectId } : {}),
|
|
67
90
|
...endpointPayload(canonicalRow),
|
|
68
91
|
})
|
|
92
|
+
return assertMutationOK(res, "create", canonicalRow.name)
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
async function applyUpdate(canonicalRow, endpointId, projectId) {
|
|
72
|
-
|
|
96
|
+
const res = await proxyToolCall("update_endpoint", {
|
|
73
97
|
...(projectId ? { project_id: projectId } : {}),
|
|
74
98
|
endpoint_id: endpointId,
|
|
75
99
|
updates: endpointPayload(canonicalRow),
|
|
76
100
|
})
|
|
101
|
+
return assertMutationOK(res, "update", canonicalRow.name)
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
async function applyDelete(endpointId, projectId) {
|
|
80
|
-
|
|
105
|
+
const res = await proxyToolCall("delete_endpoint", {
|
|
81
106
|
...(projectId ? { project_id: projectId } : {}),
|
|
82
107
|
endpoint_id: endpointId,
|
|
83
108
|
})
|
|
109
|
+
return assertMutationOK(res, "delete", endpointId)
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
// ─── Realtime policies sync ────────────────────────────────────────────────
|
|
@@ -358,6 +384,13 @@ export const dypaiPushTool = {
|
|
|
358
384
|
errors.push({ op: "realtime", error: e.message })
|
|
359
385
|
}
|
|
360
386
|
|
|
387
|
+
// Names of endpoints that actually changed this run — drives the follow-up
|
|
388
|
+
// suggestion so the agent knows what to test next.
|
|
389
|
+
const changedNames = [
|
|
390
|
+
...applied.created,
|
|
391
|
+
...applied.updated.map(u => u.name),
|
|
392
|
+
]
|
|
393
|
+
|
|
361
394
|
return {
|
|
362
395
|
success: errors.length === 0,
|
|
363
396
|
applied: true,
|
|
@@ -375,7 +408,7 @@ export const dypaiPushTool = {
|
|
|
375
408
|
next_step: errors.length
|
|
376
409
|
? "Fix the offending YAMLs and push again."
|
|
377
410
|
: changedNames.length
|
|
378
|
-
? `Test changed endpoint(s) with
|
|
411
|
+
? `Test changed endpoint(s) with dypai_test_endpoint: ${changedNames.slice(0, 3).join(", ")}${changedNames.length > 3 ? "…" : ""}`
|
|
379
412
|
: undefined,
|
|
380
413
|
hint: errors.some(e => /credential/i.test(e.error))
|
|
381
414
|
? "A referenced credential doesn't exist remotely. Create it in the dashboard (same name), then retry."
|