@dypai-ai/mcp 1.5.5 → 1.5.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "description": "DYPAI MCP Server — AI agent toolkit for building and deploying full-stack apps",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -115,6 +115,7 @@ const REMOTE_TOOLS = [
115
115
  // ── Project ───────────────────────────────────────────────────────────────
116
116
  { name: "list_projects", description: "Lists all projects you have access to across your organizations. Returns project id, name, description, organization, subscription plan, and status. Use this as the first step to discover which projects are available, then pass project_id to other tools.", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "Optional. Filter projects by organization UUID." } }, required: [] } },
117
117
  { name: "get_project", description: "Gets detailed information about a specific project. Returns project name, description, organization, plan, status, engine URL, frontend slug, and timestamps.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] } },
118
+ { name: "list_ai_models", description: "List only the DYPAI Managed AI models that are active for a project. Returns the project-gated OpenRouter model catalog, monthly included AI credits, monthly hard cap, RPM limit, max output tokens, active/available counts, and the exact node parameters to use. Call this before creating or editing an AI Agent node with DYPAI Managed models. Agents must not invent or use inactive model ids. Use provider='openrouter' and do NOT set credential_id; DYPAI uses the platform OpenRouter key and bills usage to the organization.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project UUID whose plan and Model Gateway settings determine the active Managed AI catalog." } }, required: ["project_id"] } },
118
119
  { name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, engine, GitHub repo, and frontend hosting. BLOCKS by default until provisioning finishes (~60s typical, 120s max) — when it returns, the project_id is ready to use with execute_sql, endpoint tools, etc. Pass wait_until_ready:false for batch flows.\n\nName collision: if another project in the same org already uses the name (case-insensitive), returns {error:'name_taken', existing_project_id, suggestions:[...]}. Pick a different name or use the existing project.\n\nIMPORTANT: before calling, check for a matching template with `search_project_templates`. Passing a `template_slug` drops in a ready-made schema + endpoints + UI that cover 70% of common app types. Only create a blank project if nothing matches.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "RECOMMENDED. Project template slug to start from (e.g. 'clinic', 'gym', 'waitlist', 'blank'). Always call search_project_templates first to find the best match." }, wait_until_ready: { type: "boolean", description: "If true (default), blocks until provisioning completes and the project is ready for all operations. If false, returns immediately with status='provisioning' — caller must poll get_project before using.", default: true } }, required: ["name"] } },
119
120
  { name: "get_app_credentials", description: "Lists available credentials in the current application. Returns API keys, anon key, service role key, and engine URL needed for SDK configuration.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
120
121
 
@@ -16,6 +16,70 @@ const MCP_ENDPOINT = `${MCP_BASE}/mcp`
16
16
 
17
17
  let sessionId = null
18
18
 
19
+ function extractLastSseData(text) {
20
+ const events = text.split(/\r?\n\r?\n/).filter(Boolean)
21
+ for (let i = events.length - 1; i >= 0; i--) {
22
+ const dataLines = events[i]
23
+ .split(/\r?\n/)
24
+ .filter((line) => line.startsWith("data:"))
25
+ .map((line) => line.replace(/^data:\s?/, ""))
26
+ .filter((line) => line && line !== "[DONE]")
27
+ if (dataLines.length) return dataLines.join("\n")
28
+ }
29
+ return null
30
+ }
31
+
32
+ function decodeTransportValue(value) {
33
+ let current = value
34
+ for (let i = 0; i < 5; i++) {
35
+ if (typeof current !== "string") return current
36
+ const trimmed = current.trim()
37
+ const sseData = extractLastSseData(trimmed)
38
+ const candidate = sseData || trimmed
39
+ try {
40
+ current = JSON.parse(candidate)
41
+ } catch {
42
+ return current
43
+ }
44
+ }
45
+ return current
46
+ }
47
+
48
+ function normalizeJsonRpcResponse(response) {
49
+ let current = decodeTransportValue(response)
50
+
51
+ for (let i = 0; i < 5; i++) {
52
+ if (!current || typeof current !== "object") return current
53
+
54
+ // Some HTTP/SSE transports get wrapped as { result: "<jsonrpc...>" } after
55
+ // a fallback parse path. Unwrap that so callers always see the real MCP
56
+ // JSON-RPC response instead of a stringified envelope.
57
+ if (
58
+ Object.keys(current).length === 1
59
+ && typeof current.result === "string"
60
+ ) {
61
+ const decoded = decodeTransportValue(current.result)
62
+ if (decoded !== current.result) {
63
+ current = decoded
64
+ continue
65
+ }
66
+ }
67
+
68
+ if (
69
+ current.result
70
+ && typeof current.result === "object"
71
+ && current.result.jsonrpc
72
+ ) {
73
+ current = current.result
74
+ continue
75
+ }
76
+
77
+ return current
78
+ }
79
+
80
+ return current
81
+ }
82
+
19
83
  function mcpRequest(body) {
20
84
  const token = process.env.DYPAI_TOKEN || ""
21
85
 
@@ -50,18 +114,9 @@ function mcpRequest(body) {
50
114
  res.on("end", () => {
51
115
  if (res.statusCode >= 200 && res.statusCode < 300) {
52
116
  // Handle SSE responses (text/event-stream)
53
- if (buf.includes("data: ")) {
54
- const lines = buf.split("\n").filter(l => l.startsWith("data: "))
55
- const lastData = lines[lines.length - 1]
56
- if (lastData) {
57
- try {
58
- resolve(JSON.parse(lastData.replace("data: ", "")))
59
- } catch {
60
- resolve({ result: buf })
61
- }
62
- } else {
63
- resolve({ result: buf })
64
- }
117
+ const sseData = extractLastSseData(buf)
118
+ if (sseData) {
119
+ try { resolve(JSON.parse(sseData)) } catch { resolve({ result: sseData }) }
65
120
  } else {
66
121
  try { resolve(JSON.parse(buf)) } catch { resolve({ result: buf }) }
67
122
  }
@@ -116,7 +171,7 @@ async function ensureInitialized() {
116
171
  export async function proxyToolCall(toolName, args) {
117
172
  await ensureInitialized()
118
173
 
119
- const response = await mcpRequest({
174
+ const response = normalizeJsonRpcResponse(await mcpRequest({
120
175
  jsonrpc: "2.0",
121
176
  id: `proxy-${Date.now()}`,
122
177
  method: "tools/call",
@@ -124,7 +179,7 @@ export async function proxyToolCall(toolName, args) {
124
179
  name: toolName,
125
180
  arguments: args || {},
126
181
  },
127
- })
182
+ }))
128
183
 
129
184
  // Extract result from JSON-RPC response
130
185
  if (response.result) {
@@ -137,7 +192,7 @@ export async function proxyToolCall(toolName, args) {
137
192
 
138
193
  if (text != null) {
139
194
  let parsed
140
- try { parsed = JSON.parse(text) } catch { parsed = text }
195
+ parsed = decodeTransportValue(text)
141
196
  // Some remote errors come as a plain string without isError flag
142
197
  if (typeof parsed === "string" && /^(Error:|Input validation error:|You don't have)/i.test(parsed)) {
143
198
  throw new Error(parsed)