Skip to content

Commit 0968485

Browse files
committed
sync
1 parent 74d2d8b commit 0968485

File tree

21 files changed

+1124
-226
lines changed

21 files changed

+1124
-226
lines changed

app/core/src/workspace/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export namespace Workspace {
1818
workspaceID,
1919
id: Identifier.create("user"),
2020
email: account.properties.email,
21+
name: "",
2122
})
2223
})
2324
return workspaceID

app/function/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dependencies": {
88
"@ai-sdk/anthropic": "1.2.1",
99
"@hono/zod-validator": "0.4.3",
10+
"@modelcontextprotocol/sdk": "1.9.0",
1011
"@openauthjs/openauth": "0.0.0-20250322224806",
1112
"@opencontrol/core": "workspace:*",
1213
"@rocicorp/zero": "0.17.2025031904",

app/function/src/api.ts

Lines changed: 175 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { createClient } from "@openauthjs/openauth/client"
33
import { Actor } from "@opencontrol/core/actor.js"
44
import { Log } from "@opencontrol/core/util/log.js"
55
import { Workspace } from "@opencontrol/core/workspace/index.js"
6-
import { Database, eq } from "@opencontrol/core/drizzle/index.js"
7-
import { Hono } from "hono"
6+
import { Database, eq, and } from "@opencontrol/core/drizzle/index.js"
7+
import { Hono, MiddlewareHandler } from "hono"
88
import { handle } from "hono/aws-lambda"
99
import { HTTPException } from "hono/http-exception"
1010
import { Resource } from "sst"
@@ -16,6 +16,13 @@ import { WorkspaceTable } from "@opencontrol/core/workspace/workspace.sql.js"
1616
import { AwsAccountTable } from "@opencontrol/core/aws.sql.js"
1717
import { Identifier } from "@opencontrol/core/identifier.js"
1818
import { Aws } from "@opencontrol/core/aws.js"
19+
import {
20+
ListToolsRequestSchema,
21+
CallToolRequestSchema,
22+
ListToolsResult,
23+
CallToolResult,
24+
} from "@modelcontextprotocol/sdk/types.js"
25+
import { UserTable } from "@opencontrol/core/user/user.sql.js"
1926

2027
const model = createAnthropic({
2128
apiKey: Resource.AnthropicApiKey.value,
@@ -30,30 +37,61 @@ const log = Log.create({
3037
namespace: "api",
3138
})
3239

33-
const app = new Hono()
34-
.use(async (c, next) => {
35-
const authorization = c.req.header("Authorization")
36-
if (authorization) {
37-
const [type, token] = authorization.split(" ")
38-
if (type !== "Bearer") {
39-
throw new HTTPException(401, {
40-
message: "Invalid authorization header",
41-
})
42-
}
43-
const verified = await client.verify(token)
44-
if (verified.err)
45-
throw new HTTPException(401, {
46-
message: verified.err.message,
47-
})
40+
export const AuthMiddleware: MiddlewareHandler = async (c, next) => {
41+
const authorization = c.req.header("authorization")
42+
if (!authorization) {
43+
return Actor.provide("public", {}, next)
44+
}
45+
const token = authorization.split(" ")[1]
46+
if (!token)
47+
throw new HTTPException(403, {
48+
message: "Bearer token is required.",
49+
})
4850

49-
return Actor.provide(
50-
verified.subject.type as any,
51-
verified.subject.properties,
52-
next,
51+
const verified = await client.verify(token)
52+
if (verified.err) {
53+
throw new HTTPException(403, {
54+
message: "Invalid token.",
55+
})
56+
}
57+
let subject = verified.subject as Actor.Info
58+
if (subject.type === "account") {
59+
const workspaceID = c.req.header("x-opencontrol-workspace")
60+
const email = subject.properties.email
61+
if (workspaceID) {
62+
const user = await Database.use((tx) =>
63+
tx
64+
.select({
65+
id: UserTable.id,
66+
workspaceID: UserTable.workspaceID,
67+
})
68+
.from(UserTable)
69+
.where(
70+
and(
71+
eq(UserTable.email, email),
72+
eq(UserTable.workspaceID, workspaceID),
73+
),
74+
)
75+
.then((rows) => rows[0]),
5376
)
77+
if (!user)
78+
throw new HTTPException(403, {
79+
message: "You do not have access to this workspace.",
80+
})
81+
subject = {
82+
type: "user",
83+
properties: {
84+
userID: user.id,
85+
workspaceID: workspaceID,
86+
},
87+
}
5488
}
55-
return Actor.provide("public", {}, next)
56-
})
89+
}
90+
await Actor.provide(subject.type, subject.properties, next)
91+
}
92+
93+
const app = new Hono()
94+
.use(AuthMiddleware)
5795
.get("/rest/account", async (c, next) => {
5896
const account = Actor.assert("account")
5997
let workspaces = await Workspace.list()
@@ -141,103 +179,132 @@ const app = new Hono()
141179
})
142180
},
143181
)
144-
.post("/tools/list", async (c) => {
145-
const user = Actor.assert("user")
146-
const tools = []
147-
148-
// Look up aws tool
149-
const awsAccount = await Database.use((tx) =>
150-
tx
151-
.select({})
152-
.from(AwsAccountTable)
153-
.where(eq(AwsAccountTable.workspaceID, user.properties.workspaceID)),
154-
)
155-
if (awsAccount) {
156-
tools.push({
157-
name: "aws",
158-
description: `This uses aws sdk v2 in javascript to execute aws commands
159-
this is roughly how it works
160-
\`\`\`js
161-
import aws from "aws-sdk";
162-
aws[service][method](params)
163-
\`\`\`
164-
`,
165-
args: z.object({
166-
service: z
167-
.string()
168-
.describe(
169-
"name of the aws service in the format aws sdk v2 uses, like S3 or EC2",
170-
),
171-
method: z
172-
.string()
173-
.describe("name of the aws method in the format aws sdk v2 uses"),
174-
params: z
175-
.string()
176-
.describe("params for the aws method in json format"),
177-
}),
178-
})
179-
}
180-
181-
return c.json({ tools })
182-
})
183182
.post(
184-
"/tools/call",
183+
"/mcp",
185184
zValidator(
186185
"json",
187-
z.custom<{
188-
name: string
189-
service: string
190-
method: string
191-
params: string
192-
}>(),
186+
z.discriminatedUnion("method", [
187+
ListToolsRequestSchema,
188+
CallToolRequestSchema,
189+
]),
193190
),
194191
async (c) => {
195192
const body = c.req.valid("json")
196-
const { name, service, method, params } = body
197-
198-
if (name === "aws") {
199-
const awsAccount = await Database.use((tx) =>
200-
tx
201-
.select({
202-
accountNumber: AwsAccountTable.accountNumber,
203-
region: AwsAccountTable.region,
204-
})
205-
.from(AwsAccountTable)
206-
.where(eq(AwsAccountTable.workspaceID, Actor.workspace())),
207-
)
208-
if (!awsAccount) {
209-
throw new Error(
210-
"AWS integration not found. Please connect your AWS account first.",
211-
)
193+
switch (body.method) {
194+
case "tools/list": {
195+
const result: ListToolsResult = {
196+
tools: await Database.use((tx) =>
197+
tx
198+
.select({})
199+
.from(AwsAccountTable)
200+
.where(eq(AwsAccountTable.workspaceID, Actor.workspace())),
201+
).then((rows) =>
202+
rows.map((item): ListToolsResult["tools"][number] => ({
203+
name: "aws",
204+
description: `This uses aws sdk v2 in javascript to execute aws commands
205+
this is roughly how it works
206+
\`\`\`js
207+
import aws from "aws-sdk";
208+
aws[service][method](params)
209+
\`\`\``,
210+
inputSchema: {
211+
type: "object",
212+
properties: {
213+
service: {
214+
type: "string",
215+
description:
216+
"name of the aws service in the format aws sdk v2 uses, like S3 or EC2",
217+
},
218+
method: {
219+
type: "string",
220+
description:
221+
"name of the aws method in the format aws sdk v2 uses",
222+
},
223+
params: {
224+
type: "string",
225+
description: "params for the aws method in json format",
226+
},
227+
},
228+
},
229+
})),
230+
),
231+
}
232+
return c.json(result)
212233
}
234+
case "tools/call": {
235+
try {
236+
if (body.params.name === "aws") {
237+
const awsAccount = await Database.use((tx) =>
238+
tx
239+
.select({
240+
accountNumber: AwsAccountTable.accountNumber,
241+
region: AwsAccountTable.region,
242+
})
243+
.from(AwsAccountTable)
244+
.where(eq(AwsAccountTable.workspaceID, Actor.workspace())),
245+
)
246+
if (!awsAccount) {
247+
throw new Error(
248+
"AWS integration not found. Please connect your AWS account first.",
249+
)
250+
}
213251

214-
// Assume role
215-
const credentials = await Aws.assumeRole({
216-
accountNumber: awsAccount[0].accountNumber,
217-
region: awsAccount[0].region,
218-
})
252+
// Assume role
253+
const credentials = await Aws.assumeRole({
254+
accountNumber: awsAccount[0].accountNumber,
255+
region: awsAccount[0].region,
256+
})
219257

220-
/* @ts-expect-error */
221-
const client = aws.default[service]
222-
if (!client) {
223-
throw new HTTPException(500, {
224-
message: `service "${service}" is not found in aws sdk v2`,
225-
})
226-
}
227-
const instance = new client({
228-
credentials: {
229-
accessKeyId: credentials.AccessKeyId,
230-
secretAccessKey: credentials.SecretAccessKey,
231-
sessionToken: credentials.SessionToken,
232-
},
233-
})
234-
if (!instance[method]) {
258+
const { service, method, params } = body.params.arguments || {}
259+
260+
/* @ts-expect-error */
261+
const client = AWS[service]
262+
if (!client) {
263+
throw new Error(
264+
`service "${service}" is not found in aws sdk v2`,
265+
)
266+
}
267+
const instance = new client({
268+
credentials: {
269+
accessKeyId: credentials.AccessKeyId,
270+
secretAccessKey: credentials.SecretAccessKey,
271+
sessionToken: credentials.SessionToken,
272+
},
273+
})
274+
if (!instance[method]) {
275+
throw new Error(
276+
`method "${method}" is not found in on the ${service} service of aws sdk v2`,
277+
)
278+
}
279+
const response = await instance[method](
280+
JSON.parse(params as string),
281+
).promise()
282+
const result: CallToolResult = {
283+
content: [
284+
{
285+
type: "text",
286+
text: JSON.stringify(response),
287+
},
288+
],
289+
}
290+
return c.json(result)
291+
}
292+
} catch (error: any) {
293+
const result: CallToolResult = {
294+
isError: true,
295+
content: [
296+
{
297+
type: "text",
298+
text: error.toString(),
299+
},
300+
],
301+
}
302+
return c.json(result)
303+
}
235304
throw new HTTPException(500, {
236-
message: `method "${method}" is not found in on the ${service} service of aws sdk v2`,
305+
message: `tool "${body.params.name}" is not found`,
237306
})
238307
}
239-
const response = await instance[method](JSON.parse(params)).promise()
240-
return c.json(response)
241308
}
242309
},
243310
)

app/function/src/migrator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Database } from "@opencontrol/core/drizzle/index.js"
2+
import { migrate } from "drizzle-orm/postgres-js/migrator"
3+
4+
export const handler = async (_event: any) => {
5+
await Database.use((db) =>
6+
migrate(db, {
7+
migrationsFolder: "./migrations",
8+
}),
9+
)
10+
}

app/web/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function App(props: { url?: string }) {
1717
<DialogProvider>
1818
<DialogString />
1919
<DialogSelect />
20-
<OpenAuthProvider clientID="web" issuer={import.meta.env.VITE_AUTH_URL}>
20+
<OpenAuthProvider clientID="web" issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}>
2121
<AccountProvider>
2222
<MetaProvider>
2323
<Router

app/web/src/components/context-theme.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createStore } from "solid-js/store";
22
import { makePersisted } from "@solid-primitives/storage";
33
import { createEffect } from "solid-js";
44
import { createInitializedContext } from "../util/context";
5+
import { isServer } from "solid-js/web";
56

67
interface Storage {
78
mode: "light" | "dark";
@@ -12,8 +13,8 @@ export const { provider: ThemeProvider, use: useTheme } =
1213
const [store, setStore] = makePersisted(
1314
createStore<Storage>({
1415
mode:
15-
window.matchMedia &&
16-
window.matchMedia("(prefers-color-scheme: dark)").matches
16+
(!isServer && window.matchMedia &&
17+
window.matchMedia("(prefers-color-scheme: dark)").matches)
1718
? "dark"
1819
: "light",
1920
}),

app/web/src/entry-client.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ if (import.meta.env.DEV) {
88
}
99

1010
if (!import.meta.env.DEV) {
11-
hydrate(() => <App />, document.getElementById('root')!);
11+
if (globalThis.$HY)
12+
hydrate(() => <App />, document.getElementById('root')!);
13+
else
14+
render(() => <App />, document.getElementById('root')!);
1215
}

app/web/src/pages/[workspace]/billing.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Layout from "./components/Layout"
1+
import Layout from "./components/layout"
22

33
export default function Billing() {
44
return (

0 commit comments

Comments
 (0)