@mandujs/mcp 0.30.0 → 0.32.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/package.json +3 -3
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +300 -0
- package/src/server.ts +2 -1
- package/src/tools/ai-brief.ts +443 -443
- package/src/tools/decisions.ts +270 -270
- package/src/tools/docs.ts +349 -0
- package/src/tools/extract-contract.ts +406 -406
- package/src/tools/guard.ts +56 -3
- package/src/tools/index.ts +8 -0
- package/src/tools/lint.ts +226 -0
- package/src/tools/migrate-route-conventions.ts +345 -345
- package/src/tools/resource.ts +2 -1
- package/src/tools/rewrite-generated-barrel.ts +403 -403
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +0 -323
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Supabase as Postgres provider
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Managed Postgres + optional Realtime / Storage / Auth — Mandu consumes it as a connection string, not a deploy target
|
|
5
|
+
tags: database, postgres, supabase, baas
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Supabase as Postgres provider
|
|
9
|
+
|
|
10
|
+
> **Supabase 는 Mandu 의 배포 타겟이 아닙니다.** Mandu 앱은 Render / Fly / Railway /
|
|
11
|
+
> Vercel / Docker 등으로 배포하고, Supabase 는 **Postgres 제공자 + 선택적
|
|
12
|
+
> BaaS 기능** (Realtime, Storage, Auth, Edge Functions) 으로만 사용합니다.
|
|
13
|
+
> Mandu 의 `@mandujs/core/db` 가 `Bun.SQL` wrapper 이기 때문에 Supabase 의
|
|
14
|
+
> pooler URL 을 그대로 꽂으면 별도 SDK 없이 바로 동작합니다.
|
|
15
|
+
|
|
16
|
+
## 1. 가장 단순한 경로 — DB-only (권장)
|
|
17
|
+
|
|
18
|
+
Supabase 를 Postgres 로만 쓰는 경우. Mandu 의 네이티브 DB 레이어가 그대로 커버합니다.
|
|
19
|
+
|
|
20
|
+
### 1.1 Connection string 획득
|
|
21
|
+
|
|
22
|
+
Supabase Dashboard → Settings → Database → **Connection pooling**
|
|
23
|
+
- `Transaction` 모드 URL 복사 (포트 `6543`)
|
|
24
|
+
- 형식: `postgres://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres`
|
|
25
|
+
|
|
26
|
+
### 1.2 `.env` 에 저장
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
DATABASE_URL=postgres://postgres.xxxx:your-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 1.3 Mandu DB 핸들 생성
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// lib/db.ts
|
|
36
|
+
import { createDb } from "@mandujs/core/db";
|
|
37
|
+
|
|
38
|
+
export const db = createDb({
|
|
39
|
+
url: process.env.DATABASE_URL!,
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1.4 Filling 에서 쿼리
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// app/users/[id]/route.ts
|
|
47
|
+
import { defineFilling, notFound } from "@mandujs/core/filling";
|
|
48
|
+
import { db } from "@/lib/db";
|
|
49
|
+
|
|
50
|
+
export const filling = defineFilling({
|
|
51
|
+
async loader({ params }) {
|
|
52
|
+
const user = await db.one<{ id: string; email: string; name: string }>`
|
|
53
|
+
SELECT id, email, name FROM users WHERE id = ${params.id}
|
|
54
|
+
`;
|
|
55
|
+
if (!user) throw notFound();
|
|
56
|
+
return { user };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
트랜잭션:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
await db.transaction(async (tx) => {
|
|
65
|
+
const user = await tx.one<{ id: string }>`
|
|
66
|
+
INSERT INTO users (email, name) VALUES (${email}, ${name}) RETURNING id
|
|
67
|
+
`;
|
|
68
|
+
await tx`INSERT INTO profiles (user_id, bio) VALUES (${user!.id}, ${bio})`;
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**필요한 Supabase-specific 의존성**: 없음. `@mandujs/core/db` 만 사용. Supabase 가 바뀌어도 connection string 교체뿐.
|
|
73
|
+
|
|
74
|
+
## 2. 마이그레이션
|
|
75
|
+
|
|
76
|
+
Mandu 의 기본 마이그레이션 러너를 사용하거나 (`@mandujs/core/db/migrations`), Supabase CLI 를 병행할 수 있습니다.
|
|
77
|
+
|
|
78
|
+
### Mandu 마이그레이션 (권장)
|
|
79
|
+
|
|
80
|
+
```sql
|
|
81
|
+
-- migrations/001_users.sql
|
|
82
|
+
CREATE TABLE users (
|
|
83
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
84
|
+
email TEXT UNIQUE NOT NULL,
|
|
85
|
+
name TEXT,
|
|
86
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun run mandu db migrate
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Supabase CLI 병행
|
|
95
|
+
|
|
96
|
+
RLS (Row Level Security) / policies 등 Supabase-native 기능이 필요한 경우:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install -g supabase
|
|
100
|
+
supabase login
|
|
101
|
+
supabase init
|
|
102
|
+
supabase migration new create_users_table
|
|
103
|
+
# edit supabase/migrations/<timestamp>_create_users_table.sql
|
|
104
|
+
supabase db push
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`ALTER TABLE users ENABLE ROW LEVEL SECURITY;` 등은 plain SQL 이므로 Mandu 마이그레이션에도 그대로 작성 가능. Supabase CLI 는 RLS policy 편집 UX 와 타입 생성 (`supabase gen types typescript`) 때문에 편리합니다.
|
|
108
|
+
|
|
109
|
+
## 3. Supabase SDK 를 함께 쓰고 싶다면 (BaaS 기능)
|
|
110
|
+
|
|
111
|
+
DB 는 `@mandujs/core/db` 로, Supabase-specific 기능 (Realtime / Storage / Edge Functions) 만 SDK 로 사용하는 하이브리드가 깔끔합니다.
|
|
112
|
+
|
|
113
|
+
### 3.1 클라이언트 설정
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bun add @supabase/supabase-js
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// lib/supabase.ts
|
|
121
|
+
import { createClient } from "@supabase/supabase-js";
|
|
122
|
+
|
|
123
|
+
const supabaseUrl = process.env.SUPABASE_URL!;
|
|
124
|
+
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;
|
|
125
|
+
|
|
126
|
+
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
127
|
+
|
|
128
|
+
// 서버 사이드 (Service Role) — filling 안에서만 사용
|
|
129
|
+
export function createServerSupabase() {
|
|
130
|
+
return createClient(supabaseUrl, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
|
|
131
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3.2 Realtime (Island)
|
|
137
|
+
|
|
138
|
+
Mandu 의 client island 에서 Realtime 구독:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// app/messages/MessagesIsland.client.tsx
|
|
142
|
+
import { island } from "@mandujs/core/client";
|
|
143
|
+
import { useEffect, useState } from "react";
|
|
144
|
+
import { supabase } from "@/lib/supabase";
|
|
145
|
+
|
|
146
|
+
function Messages() {
|
|
147
|
+
const [messages, setMessages] = useState<Array<{ id: string; content: string }>>([]);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
supabase.from("messages").select("*").then(({ data }) => {
|
|
151
|
+
setMessages(data ?? []);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const channel = supabase
|
|
155
|
+
.channel("messages")
|
|
156
|
+
.on(
|
|
157
|
+
"postgres_changes",
|
|
158
|
+
{ event: "INSERT", schema: "public", table: "messages" },
|
|
159
|
+
(payload) => {
|
|
160
|
+
setMessages((prev) => [...prev, payload.new as { id: string; content: string }]);
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
.subscribe();
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
supabase.removeChannel(channel);
|
|
167
|
+
};
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ul>
|
|
172
|
+
{messages.map((m) => <li key={m.id}>{m.content}</li>)}
|
|
173
|
+
</ul>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default island("visible", <Messages />);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3.3 Storage
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// filling 내에서
|
|
184
|
+
const serverSupabase = createServerSupabase();
|
|
185
|
+
|
|
186
|
+
const { data, error } = await serverSupabase.storage
|
|
187
|
+
.from("avatars")
|
|
188
|
+
.upload(`${userId}/avatar.png`, file);
|
|
189
|
+
|
|
190
|
+
const { data: { publicUrl } } = serverSupabase.storage
|
|
191
|
+
.from("avatars")
|
|
192
|
+
.getPublicUrl(`${userId}/avatar.png`);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 3.4 Edge Functions
|
|
196
|
+
|
|
197
|
+
Supabase 의 Deno 런타임에서 도는 서버리스 함수. Mandu 앱 바깥에서 별도 배포되고, Mandu 는 HTTP 로 호출만:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const res = await fetch(`${process.env.SUPABASE_URL}/functions/v1/hello`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Authorization": `Bearer ${process.env.SUPABASE_ANON_KEY}`,
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({ name: "mandu" }),
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 4. Supabase Auth 와 Mandu auth — 선택 문제
|
|
211
|
+
|
|
212
|
+
두 가지가 겹치는 영역이라 **둘 중 하나만** 씁니다:
|
|
213
|
+
|
|
214
|
+
| Mandu 의 `@mandujs/core/auth` (native) | Supabase Auth |
|
|
215
|
+
|---|---|
|
|
216
|
+
| Bun.password (argon2id) | Bcrypt + custom hashing |
|
|
217
|
+
| `@mandujs/core/middleware/session` SQLite session store | JWT + cookie |
|
|
218
|
+
| CSRF (`@mandujs/core/middleware/csrf`) | — |
|
|
219
|
+
| Mandu 프로젝트 자체 사용자 테이블 | Supabase `auth.users` 테이블 |
|
|
220
|
+
|
|
221
|
+
**추천**: Mandu 앱이 Supabase 의 다른 기능 (Realtime / Storage) 을 안 쓰면 **Mandu native auth** 가 의존성 적고 예측 가능. Supabase 의 social providers (GitHub / Google OAuth UI) 가 필요하면 Supabase Auth 를 쓰고 Mandu auth 는 bypass.
|
|
222
|
+
|
|
223
|
+
병행 사용 (Supabase Auth 만 이용하면서 Mandu filling 안에서 token 검증):
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// middleware/supabase-auth.ts
|
|
227
|
+
import { createServerSupabase } from "@/lib/supabase";
|
|
228
|
+
|
|
229
|
+
export async function supabaseAuthMiddleware(ctx) {
|
|
230
|
+
const header = ctx.headers.get("authorization");
|
|
231
|
+
if (!header?.startsWith("Bearer ")) return ctx.unauthorized("Missing token");
|
|
232
|
+
|
|
233
|
+
const token = header.slice(7);
|
|
234
|
+
const supabase = createServerSupabase();
|
|
235
|
+
const { data: { user }, error } = await supabase.auth.getUser(token);
|
|
236
|
+
if (error || !user) return ctx.unauthorized("Invalid token");
|
|
237
|
+
|
|
238
|
+
ctx.set("user", user);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 5. 환경 변수
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# .env
|
|
246
|
+
# DB connection (최소 요구사항)
|
|
247
|
+
DATABASE_URL=postgres://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:6543/postgres
|
|
248
|
+
|
|
249
|
+
# Supabase SDK 사용 시 추가
|
|
250
|
+
SUPABASE_URL=https://xxx.supabase.co
|
|
251
|
+
SUPABASE_ANON_KEY=eyJ...
|
|
252
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ... # 서버 전용, 클라이언트에 노출 금지
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 6. 배포 플랫폼과 조합
|
|
256
|
+
|
|
257
|
+
Supabase 는 DB layer 이므로 **Mandu 앱 자체는 다른 곳** 에 배포합니다. `DATABASE_URL` 만 환경 변수로 주입:
|
|
258
|
+
|
|
259
|
+
### Render (`mandu deploy --target=render` 가 자동 생성)
|
|
260
|
+
|
|
261
|
+
```yaml
|
|
262
|
+
# render.yaml
|
|
263
|
+
services:
|
|
264
|
+
- type: web
|
|
265
|
+
name: mandu-app
|
|
266
|
+
runtime: node
|
|
267
|
+
buildCommand: |
|
|
268
|
+
curl -fsSL https://bun.sh/install | bash
|
|
269
|
+
export PATH="$HOME/.bun/bin:$PATH"
|
|
270
|
+
bun install --frozen-lockfile
|
|
271
|
+
bun run build
|
|
272
|
+
startCommand: bun run start
|
|
273
|
+
envVars:
|
|
274
|
+
- key: DATABASE_URL
|
|
275
|
+
sync: false # Render 대시보드에서 Supabase pooler URL 설정
|
|
276
|
+
- key: SUPABASE_URL
|
|
277
|
+
sync: false # SDK 쓸 때만
|
|
278
|
+
- key: SUPABASE_ANON_KEY
|
|
279
|
+
sync: false
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Fly / Railway
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
fly secrets set DATABASE_URL="postgres://..." SUPABASE_URL="..." SUPABASE_ANON_KEY="..."
|
|
286
|
+
railway variables set DATABASE_URL="postgres://..." SUPABASE_URL="..." SUPABASE_ANON_KEY="..."
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 7. 주의사항
|
|
290
|
+
|
|
291
|
+
- **Transaction mode pooler (6543) 를 사용**하세요. Session mode (5432) 는 커넥션 고정이 필요한 경우에만.
|
|
292
|
+
- **Service Role key 는 서버 전용**. Client island 에서 절대 import 하지 마세요 — Mandu 의 island 번들에 포함되면 공개됩니다.
|
|
293
|
+
- **RLS 를 켜지 않은 테이블** 은 anon key 로 전체 읽기가 가능해집니다. 항상 `ENABLE ROW LEVEL SECURITY` 적용하고 policy 를 명시.
|
|
294
|
+
- **Realtime 구독은 client bundle 비용**이 큽니다 (`@supabase/supabase-js` ~60 KB). 페이지 단위로 island 분리하세요.
|
|
295
|
+
|
|
296
|
+
## Reference
|
|
297
|
+
|
|
298
|
+
- [Supabase Docs](https://supabase.com/docs)
|
|
299
|
+
- `@mandujs/core/db` — Bun.SQL wrapper, provider-agnostic. `packages/core/src/db/index.ts`
|
|
300
|
+
- Deploy adapters — `packages/cli/src/commands/deploy/adapters/{render,fly,railway,vercel,netlify,docker,docker-compose,cf-pages}.ts`
|
package/src/server.ts
CHANGED
|
@@ -29,7 +29,8 @@ import { registerBuiltinTools, getToolsSummary } from "./tools/index.js";
|
|
|
29
29
|
|
|
30
30
|
// DNA-007: 에러 처리
|
|
31
31
|
import { createToolResponse, logToolError } from "./executor/error-handler.js";
|
|
32
|
-
import { ToolExecutor
|
|
32
|
+
import type { ToolExecutor} from "./executor/tool-executor.js";
|
|
33
|
+
import { createToolExecutor } from "./executor/tool-executor.js";
|
|
33
34
|
|
|
34
35
|
// DNA-008: 로깅 통합
|
|
35
36
|
import { setupMcpLogging, teardownMcpLogging } from "./logging/mcp-transport.js";
|