@surph_ai/sdk 0.0.1
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/auth/index.js +147 -0
- package/base/vinely-base/.nvmrc +1 -0
- package/base/vinely-base/README.md +30 -0
- package/base/vinely-base/app.js +5 -0
- package/base/vinely-base/declarations.d.ts +1 -0
- package/base/vinely-base/package.json +103 -0
- package/base/vinely-base/public/css/globals-2.css +3699 -0
- package/base/vinely-base/public/css/globals.css +3695 -0
- package/base/vinely-base/public/js/app.js +1 -0
- package/base/vinely-base/public/js/jquery.js +2 -0
- package/base/vinely-base/scratch.js +21 -0
- package/base/vinely-base/src/client/components/app-sidebar.tsx +59 -0
- package/base/vinely-base/src/client/components/assistant-ui/markdown-text.tsx +153 -0
- package/base/vinely-base/src/client/components/assistant-ui/thread-list.tsx +67 -0
- package/base/vinely-base/src/client/components/assistant-ui/thread.tsx +666 -0
- package/base/vinely-base/src/client/components/assistant-ui/tool-fallback.tsx +44 -0
- package/base/vinely-base/src/client/components/assistant-ui/tooltip-icon-button.tsx +44 -0
- package/base/vinely-base/src/client/components/ui/breadcrumb.tsx +109 -0
- package/base/vinely-base/src/client/components/ui/button.tsx +59 -0
- package/base/vinely-base/src/client/components/ui/input.tsx +21 -0
- package/base/vinely-base/src/client/components/ui/separator.tsx +28 -0
- package/base/vinely-base/src/client/components/ui/sheet.tsx +139 -0
- package/base/vinely-base/src/client/components/ui/sidebar.tsx +726 -0
- package/base/vinely-base/src/client/components/ui/skeleton.tsx +14 -0
- package/base/vinely-base/src/client/components/ui/tooltip.tsx +61 -0
- package/base/vinely-base/src/client/components/vines-sidebar.tsx +182 -0
- package/base/vinely-base/src/client/contexts/SessionContext.tsx +28 -0
- package/base/vinely-base/src/client/contexts/SessionReducer.tsx +32 -0
- package/base/vinely-base/src/client/hooks/use-mobile.ts +19 -0
- package/base/vinely-base/src/client/index.tsx +154 -0
- package/base/vinely-base/src/client/lib/API.ts +155 -0
- package/base/vinely-base/src/client/lib/RPC.ts +49 -0
- package/base/vinely-base/src/client/lib/utils.ts +96 -0
- package/base/vinely-base/src/server/index.ts +63 -0
- package/base/vinely-base/src/server/routes/api.ts +38 -0
- package/base/vinely-base/src/server/routes/chat.ts +133 -0
- package/base/vinely-base/src/server/routes/main.ts +36 -0
- package/base/vinely-base/src/server/routes/mcp.ts +52 -0
- package/base/vinely-base/src/server/routes/rpc.ts +64 -0
- package/base/vinely-base/src/server/routes/thread.ts +44 -0
- package/base/vinely-base/src/server/utils/APIMgr.ts +156 -0
- package/base/vinely-base/src/server/utils/Auth.ts +235 -0
- package/base/vinely-base/src/server/utils/fetchManifest.ts +15 -0
- package/base/vinely-base/src/server/utils/mcp-auth.ts +251 -0
- package/base/vinely-base/tsconfig.json +16 -0
- package/base/vinely-base/views/landing.html +187 -0
- package/base/vinely-base/views/partials/imports.dev.html +3 -0
- package/base/vinely-base/webpack.config.js +112 -0
- package/deploy/Dockerfile +9 -0
- package/deploy/index.js +244 -0
- package/index.js +266 -0
- package/package.json +28 -0
- package/scaffold/app/gulpfile.js +12 -0
- package/scaffold/app/package.json +24 -0
- package/scaffold/app/src/index.js +14 -0
- package/scaffold/index.js +171 -0
- package/scaffold/project/README.md +28 -0
- package/scaffold/project/app.js +20 -0
- package/scaffold/project/apps/README.md +1 -0
- package/scaffold/project/env.txt +4 -0
- package/scaffold/project/package-lock.json +2459 -0
- package/scaffold/project/package.json +21 -0
- package/scaffold/project/public/css/bootstrap.css +5926 -0
- package/scaffold/project/public/js/app.js +1 -0
- package/scaffold/project/public/js/jquery.js +2 -0
- package/scaffold/project/routes/index.js +8 -0
- package/scaffold/project/templates/index.mustache +20 -0
- package/utils/Realm.js +60 -0
- package/utils/http.js +22 -0
- package/utils/index.js +276 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Response, NextFunction, Express } from 'express'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import passport from "passport"
|
|
4
|
+
import { Strategy as GoogleStrategy } from "passport-google-oauth20"
|
|
5
|
+
import APIMgr from './APIMgr.js'
|
|
6
|
+
|
|
7
|
+
const COOKIE_NAME: string = process.env.COOKIE_NAME || ''
|
|
8
|
+
const USER_CACHE: any = {}
|
|
9
|
+
|
|
10
|
+
const GOOGLE_CLIENT_ID: string = process.env.GOOGLE_CLIENT_ID as string
|
|
11
|
+
const GOOGLE_CLIENT_SECRET: string = process.env.GOOGLE_CLIENT_SECRET as string
|
|
12
|
+
const GOOGLE_CALLBACK_URL: string = process.env.GOOGLE_CALLBACK_URL as string
|
|
13
|
+
|
|
14
|
+
const generateSalt = () => {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const byteSize = 16
|
|
17
|
+
crypto.randomBytes(byteSize, (err, salt) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
reject(err)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolve(salt.toString('base64'))
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hashPw = (password: string, salt: string) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const defaultIterations = 10000
|
|
31
|
+
const defaultKeyLength = 64
|
|
32
|
+
// const saltBase64 = new Buffer(salt, 'base64')
|
|
33
|
+
const saltBase64 = Buffer.from(salt, 'base64')
|
|
34
|
+
const digest = 'sha512'
|
|
35
|
+
|
|
36
|
+
crypto.pbkdf2(
|
|
37
|
+
password,
|
|
38
|
+
saltBase64,
|
|
39
|
+
defaultIterations,
|
|
40
|
+
defaultKeyLength,
|
|
41
|
+
digest,
|
|
42
|
+
(err, key) => {
|
|
43
|
+
if (err) {
|
|
44
|
+
reject(err)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resolve(key.toString('base64'))
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const authenticate = async (password: string, profile: any) => {
|
|
55
|
+
const hashed = await hashPw(password, profile.auth.salt)
|
|
56
|
+
return (hashed === profile.auth.pw)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const setToken = (res: Response, cookieValue: string, tknName?: string) => {
|
|
60
|
+
const token = tknName || process.env.COOKIE_NAME as string
|
|
61
|
+
// console.log('\nSET TOKEN: ' + token + ': ' + cookieValue + '\n')
|
|
62
|
+
const ONE_DAY = 24 * 60 * 60 * 1000
|
|
63
|
+
const domain = (process.env.TURBO_ENV === 'local') ? 'localhost' : '.vinely.ai'
|
|
64
|
+
|
|
65
|
+
// const cookieStr = cookieValue || ''
|
|
66
|
+
res.cookie(token, cookieValue, {
|
|
67
|
+
maxAge: 14 * ONE_DAY,
|
|
68
|
+
httpOnly: true,
|
|
69
|
+
domain,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const clearToken = (res: Response, tknName?: string) => {
|
|
74
|
+
const token = tknName || process.env.COOKIE_NAME as string
|
|
75
|
+
const domain = (process.env.TURBO_ENV === 'local') ? 'localhost' : '.vinely.ai'
|
|
76
|
+
res.clearCookie(token, { domain })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sessionMiddleware = async (req: any, res: Response, next: NextFunction) => {
|
|
80
|
+
if (!req.cookies) {
|
|
81
|
+
console.log('SESSION NOT FOUND: ')
|
|
82
|
+
return next()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cookie = req.cookies[COOKIE_NAME]
|
|
86
|
+
if (!cookie) {
|
|
87
|
+
console.log('NO VINLEY TKN: ' + JSON.stringify(req.cookies))
|
|
88
|
+
return next()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (cookie === 'anon') {
|
|
92
|
+
console.log('NOT LOGGED IN: ' + JSON.stringify(req.cookies))
|
|
93
|
+
return next()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const session = JSON.parse(cookie)
|
|
98
|
+
// session.user = '68bb5f2b6d70d5c695588dd4' // hayden
|
|
99
|
+
// session.user = '68bf432cdcf8995ba8c48519' // tess
|
|
100
|
+
|
|
101
|
+
let user = USER_CACHE[session.user]
|
|
102
|
+
if (user) {
|
|
103
|
+
req._session = user
|
|
104
|
+
} else {
|
|
105
|
+
const { payload } = await APIMgr.get(`user/${session.user}`, null, null)
|
|
106
|
+
delete payload.auth
|
|
107
|
+
delete payload.email
|
|
108
|
+
|
|
109
|
+
USER_CACHE[session.user] = payload
|
|
110
|
+
req._session = payload
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
res.clearCookie(COOKIE_NAME) // invalidate token
|
|
114
|
+
} finally {
|
|
115
|
+
next()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const setupPassport = (app: Express) => {
|
|
120
|
+
passport.use(new GoogleStrategy({
|
|
121
|
+
clientID: GOOGLE_CLIENT_ID,
|
|
122
|
+
clientSecret: GOOGLE_CLIENT_SECRET,
|
|
123
|
+
// callbackURL: "http://localhost:8080/oauth/google/callback"
|
|
124
|
+
callbackURL: GOOGLE_CALLBACK_URL
|
|
125
|
+
}, (accessToken, refreshToken, profile, done) => {
|
|
126
|
+
|
|
127
|
+
// TODO: check if user exists in DB or create a new one
|
|
128
|
+
console.log('GOOGLE PROFILE: ' + JSON.stringify(profile))
|
|
129
|
+
return done(null, profile);
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
app.use(passport.initialize())
|
|
133
|
+
// app.use(passport.session())
|
|
134
|
+
|
|
135
|
+
// Serialize/deserialize user
|
|
136
|
+
passport.serializeUser((user: any, done) => {
|
|
137
|
+
done(null, user)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
passport.deserializeUser((user: any, done) => {
|
|
141
|
+
done(null, user)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.get(
|
|
145
|
+
'/oauth/google',
|
|
146
|
+
passport.authenticate("google", { scope: ["profile", "email"] })
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
app.get(
|
|
150
|
+
'/oauth/google/callback',
|
|
151
|
+
passport.authenticate("google", { session: false, failureRedirect: "/" }),
|
|
152
|
+
async (req, res) => {
|
|
153
|
+
const userData = req.user as any
|
|
154
|
+
const user = userData._json
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
{
|
|
158
|
+
"sub": "105065442288013194527",
|
|
159
|
+
"name": "Joe Rogan",
|
|
160
|
+
"given_name": "Joe",
|
|
161
|
+
"family_name": "Rogan",
|
|
162
|
+
"picture": "https://lh3.googleusercontent.com/a/ACg8ocIefrpbU2qhABOKkJW3Ro_1_47nOBnYGI2uAp4H5h4BXvIk2w=s96-c",
|
|
163
|
+
"email": "joe.rogan@gmail.com",
|
|
164
|
+
"email_verified": true
|
|
165
|
+
}
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
const { payload } = await APIMgr.get('user', { email: user.email }, null)
|
|
169
|
+
|
|
170
|
+
if (payload.length > 0) { // user already registerd
|
|
171
|
+
const currentUser = payload[0]
|
|
172
|
+
|
|
173
|
+
if (currentUser.auth.type !== 'google') {
|
|
174
|
+
res.status(403).json({
|
|
175
|
+
error: 'User not registered with google'
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setToken(res, JSON.stringify({ user: currentUser._id }))
|
|
182
|
+
res.redirect('/')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { token } = await registerUser(user, 'google')
|
|
187
|
+
setToken(res, token)
|
|
188
|
+
res.redirect('/')
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const registerUser = async (user: any, type: string) => {
|
|
194
|
+
const { email } = user
|
|
195
|
+
|
|
196
|
+
const formattedEmail = email.toLowerCase().trim()
|
|
197
|
+
const params: any = {
|
|
198
|
+
email: formattedEmail,
|
|
199
|
+
username: formattedEmail.split('@')[0]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const auth: any = { type } // email, google, linkedin, etc
|
|
203
|
+
|
|
204
|
+
if (type === 'email') {
|
|
205
|
+
const password = user.password
|
|
206
|
+
auth.salt = await generateSalt()
|
|
207
|
+
auth.pw = await hashPw(password, auth.salt)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (type === 'google') {
|
|
211
|
+
auth.googleId = user.sub
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
params.auth = auth
|
|
215
|
+
const { payload } = await APIMgr.post('user', params, null)
|
|
216
|
+
delete payload.auth
|
|
217
|
+
|
|
218
|
+
const profile = payload
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
user: profile,
|
|
222
|
+
token: JSON.stringify({ user: profile._id })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export {
|
|
227
|
+
generateSalt,
|
|
228
|
+
hashPw,
|
|
229
|
+
authenticate,
|
|
230
|
+
setToken,
|
|
231
|
+
clearToken,
|
|
232
|
+
sessionMiddleware,
|
|
233
|
+
setupPassport,
|
|
234
|
+
registerUser
|
|
235
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
export default async () => {
|
|
4
|
+
const url = `https://vinely.s3.us-east-1.amazonaws.com/sites/${process.env.VLY_APP}/manifest.txt`
|
|
5
|
+
|
|
6
|
+
const { data } = await axios({
|
|
7
|
+
url,
|
|
8
|
+
method: 'get',
|
|
9
|
+
headers: {
|
|
10
|
+
Accept: 'application/json'
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
return data
|
|
15
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { OAuthClientProvider, UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens, OAuthClientMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
|
|
6
|
+
export class WebOAuthClientProvider implements OAuthClientProvider {
|
|
7
|
+
private _redirectUrl: string | URL;
|
|
8
|
+
//private _clientMetadata: OAuthClientMetadata;
|
|
9
|
+
private _clientInformation?: OAuthClientInformation | undefined;
|
|
10
|
+
private _tokens?: OAuthTokens | undefined;
|
|
11
|
+
private _codeVerifier?: string | undefined;
|
|
12
|
+
private _redirect: (url: URL) => Promise<void>;
|
|
13
|
+
|
|
14
|
+
constructor(redirectUrl: URL,
|
|
15
|
+
redirect: (url: URL) => Promise<void>) {
|
|
16
|
+
this._redirectUrl = redirectUrl;
|
|
17
|
+
// TODO: May need to take this in dynamically to support variation
|
|
18
|
+
// amongst MCP servers
|
|
19
|
+
// this._clientMetadata = clientMetadata;
|
|
20
|
+
this._redirect = redirect;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get redirectUrl(): string | URL {
|
|
24
|
+
return this._redirectUrl
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get clientMetadata(): OAuthClientMetadata {
|
|
28
|
+
return {
|
|
29
|
+
redirect_uris: [ this._redirectUrl.toString() ],
|
|
30
|
+
scope: "mcp:tools",
|
|
31
|
+
token_endpoint_auth_method: "client_secret_post",
|
|
32
|
+
grant_types: [ "authorization_code", "refresh_token" ],
|
|
33
|
+
response_types: [ "code" ],
|
|
34
|
+
client_name: "Test Web OAuth Client",
|
|
35
|
+
client_uri: undefined, // TODO: Add client URI
|
|
36
|
+
|
|
37
|
+
// JWKS auth
|
|
38
|
+
jwks_uri: undefined,
|
|
39
|
+
jwks: undefined,
|
|
40
|
+
|
|
41
|
+
// Other identifying fields
|
|
42
|
+
logo_uri: undefined, // TODO: Add logo URI
|
|
43
|
+
tos_uri: undefined, // TODO: Add TOS URI
|
|
44
|
+
policy_uri: undefined, // TODO: Add policy URI
|
|
45
|
+
contacts: undefined, // TODO: Add contacts
|
|
46
|
+
software_id: undefined, // TODO: Add software ID
|
|
47
|
+
software_version: "0.1.0", // TODO: Add software version
|
|
48
|
+
software_statement: undefined, // TODO: Add software statement
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// TODO: State parameters are a good thing?
|
|
53
|
+
// state?(): string | Promise<string> {
|
|
54
|
+
// throw new Error("Method not implemented.");
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
clientInformation(): OAuthClientInformation | undefined |
|
|
58
|
+
Promise<OAuthClientInformation | undefined> {
|
|
59
|
+
return this._clientInformation;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
saveClientInformation?(clientInformation: OAuthClientInformationFull): void | Promise<void> {
|
|
63
|
+
this._clientInformation = clientInformation;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined> {
|
|
67
|
+
return this._tokens;
|
|
68
|
+
}
|
|
69
|
+
saveTokens(tokens: OAuthTokens): void | Promise<void> {
|
|
70
|
+
this._tokens = tokens;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
redirectToAuthorization(authorizationUrl: URL): void | Promise<void> {
|
|
74
|
+
this._redirect(authorizationUrl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
codeVerifier(): string | Promise<string> {
|
|
78
|
+
if (!this._codeVerifier) {
|
|
79
|
+
throw new Error("Code verifier not set.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this._codeVerifier;
|
|
83
|
+
}
|
|
84
|
+
saveCodeVerifier(codeVerifier: string): void | Promise<void> {
|
|
85
|
+
this._codeVerifier = codeVerifier;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// addClientAuthentication?(headers: Headers, params: URLSearchParams, url: string | URL, metadata?: AuthorizationServerMetadata): void | Promise<void> {
|
|
89
|
+
// throw new Error("Method not implemented.");
|
|
90
|
+
// }
|
|
91
|
+
// validateResourceURL?(serverUrl: string | URL, resource?: string): Promise<URL | undefined> {
|
|
92
|
+
// throw new Error("Method not implemented.");
|
|
93
|
+
// }
|
|
94
|
+
// invalidateCredentials?(scope: "all" | "client" | "tokens" | "verifier"): void | Promise<void> {
|
|
95
|
+
// throw new Error("Method not implemented.");
|
|
96
|
+
// }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// NextJs blows this shit away while chunking
|
|
100
|
+
// const mcpAuthProviders = new Map<URL, WebOAuthClientProvider>();
|
|
101
|
+
type McpAuthProvidersStore = Map<string, WebOAuthClientProvider>;
|
|
102
|
+
|
|
103
|
+
type MCPActiveAuthFlow = {
|
|
104
|
+
clientName: string,
|
|
105
|
+
url: URL,
|
|
106
|
+
transport: StreamableHTTPClientTransport,
|
|
107
|
+
mcpClient: Client
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// NextJs blows this shit away while chunking
|
|
111
|
+
//const mcpAuthFlows = new Map<string, MCPActiveAuthFlow>();
|
|
112
|
+
type McpAuthFlowsStore = Map<string, MCPActiveAuthFlow>;
|
|
113
|
+
|
|
114
|
+
// Extend globalThis to include some extra state
|
|
115
|
+
declare global {
|
|
116
|
+
var __mcpAuthFlows: McpAuthFlowsStore | undefined;
|
|
117
|
+
var __mcpProviders: McpAuthProvidersStore | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const mcpProviders: McpAuthProvidersStore = (globalThis.__mcpProviders
|
|
121
|
+
?? (globalThis.__mcpProviders = new Map<string, WebOAuthClientProvider>())) as McpAuthProvidersStore;
|
|
122
|
+
|
|
123
|
+
const mcpAuthFlows: McpAuthFlowsStore = (globalThis.__mcpAuthFlows
|
|
124
|
+
?? (globalThis.__mcpAuthFlows = new Map<string, MCPActiveAuthFlow>())) as McpAuthFlowsStore;
|
|
125
|
+
|
|
126
|
+
export const hasAuthProvider = (mcpUrl: URL): boolean => {
|
|
127
|
+
const url = mcpUrl.toString()
|
|
128
|
+
if (!mcpProviders.has(url)) {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const getAuthProvider = (mcpUrl: URL): any => {
|
|
136
|
+
const url = mcpUrl.toString();
|
|
137
|
+
if (!mcpProviders.has(url)) {
|
|
138
|
+
// throw new Error("No auth provider found for MCP URL: " + mcpUrl);
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return mcpProviders.get(url) as WebOAuthClientProvider;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const setupAuthProvider = async (mcpClientName: string, mcpUrl: URL,
|
|
146
|
+
version: string = "0.0.1"): Promise<URL | undefined> =>
|
|
147
|
+
{
|
|
148
|
+
console.log("setupAuthProvider");
|
|
149
|
+
|
|
150
|
+
// TODO: Generate unique cryptographically secure flow IDs to prevent auth flow collisions
|
|
151
|
+
const flowId = "abc123";
|
|
152
|
+
|
|
153
|
+
let redirectUrl = undefined;
|
|
154
|
+
|
|
155
|
+
// Create a new provider
|
|
156
|
+
const provider = new WebOAuthClientProvider(
|
|
157
|
+
// new URL(`http://localhost:3000/api/v1/mcp/auth/${flowId}`),
|
|
158
|
+
new URL(`http://localhost:${process.env.PORT}/api/mcp/auth`),
|
|
159
|
+
async (url: URL) => {
|
|
160
|
+
console.debug(`Redirecting to: ${url}`);
|
|
161
|
+
|
|
162
|
+
// Closure; we'll await later and return this
|
|
163
|
+
redirectUrl = url;
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Trigger the authentication flow by attempting to connect
|
|
168
|
+
const transport = new StreamableHTTPClientTransport(mcpUrl, {
|
|
169
|
+
authProvider: provider,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const mcpClient = new Client({
|
|
173
|
+
name: mcpClientName,
|
|
174
|
+
version,
|
|
175
|
+
}, { capabilities: {} });
|
|
176
|
+
|
|
177
|
+
// Persist some stateful bits
|
|
178
|
+
// TODO: Consider how to serialize these in a serverless manner and determine the extent
|
|
179
|
+
// to which they even need to be serialized
|
|
180
|
+
mcpProviders.set(mcpUrl.toString(), provider)
|
|
181
|
+
mcpAuthFlows.set(flowId, {
|
|
182
|
+
clientName: mcpClientName,
|
|
183
|
+
url: mcpUrl,
|
|
184
|
+
transport,
|
|
185
|
+
mcpClient
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await mcpClient.connect(transport);
|
|
190
|
+
|
|
191
|
+
// Authentication was successful, apparently
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof UnauthorizedError) {
|
|
195
|
+
// Expected failure; authentication is in progress (presumably); return the redirect
|
|
196
|
+
if (redirectUrl) {
|
|
197
|
+
return redirectUrl
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error("Expected a redirect URL to be generated.");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.error("Error establishing initial MCP connection:", error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
// Close the connection - for now
|
|
209
|
+
await mcpClient.close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const handleMCPAuthCallback = async (req: any, res: any) => {
|
|
214
|
+
console.log("handleMCPAuthCallback");
|
|
215
|
+
const { code } = req.query
|
|
216
|
+
if (!code) {
|
|
217
|
+
res.status(400).send('Missing code')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// TODO: Parameterize this
|
|
222
|
+
if (!mcpAuthFlows.has("abc123")) {
|
|
223
|
+
res.status(400).send('No active authentication flow found.')
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { url, clientName, transport, mcpClient } = mcpAuthFlows.get("abc123") as MCPActiveAuthFlow;
|
|
228
|
+
|
|
229
|
+
// TODO: check if we need to reuse the same transport to do this
|
|
230
|
+
// ... upon reviewing source, I don't think we do.
|
|
231
|
+
// Just need persisted clientInformation in the provider.
|
|
232
|
+
await transport.finishAuth(code)
|
|
233
|
+
|
|
234
|
+
console.log('TEST 3: ' + JSON.stringify(mcpProviders.get(url.toString())))
|
|
235
|
+
|
|
236
|
+
// Try to connect to confirm it is setup; note: need a new transport
|
|
237
|
+
const testTransport = new StreamableHTTPClientTransport(url, {
|
|
238
|
+
authProvider: mcpProviders.get(url.toString())
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await mcpClient.connect(testTransport);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.error("Error finalizing authentication for MCP connection:", error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// res.send('Authentication complete. You can close this window.')
|
|
250
|
+
res.redirect('/')
|
|
251
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"outDir": "./build",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"module": "es2020",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/server/**/**/*", "declarations.d.ts"],
|
|
15
|
+
"exclude": ["node_modules"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html dir="ltr" lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
7
|
+
<meta http-equiv="Content-Language" content="en" />
|
|
8
|
+
|
|
9
|
+
<title>Vinely</title>
|
|
10
|
+
<link rel="shortcut icon" href="https://lh3.googleusercontent.com/MpnmfR3zYPTojA03QbhGgwIHucmH5s4G2vbIlADYhODL488MQt_BuVztv9LYmJbhwkScYN0DIXuh4d10YMsLbWXT">
|
|
11
|
+
<link rel="stylesheet" href="{{{CDN}}}/css/globals-2.css" />
|
|
12
|
+
<link rel="stylesheet" href="{{{CDN}}}/dist/css/katex.min.css" />
|
|
13
|
+
<style>
|
|
14
|
+
.m-auto {
|
|
15
|
+
margin: auto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.fw-600 {
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.reference-container {
|
|
23
|
+
transition: all 0.4s linear;
|
|
24
|
+
background: #1f2121;
|
|
25
|
+
width: 60%;
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
padding: 20px;
|
|
28
|
+
line-height: 20px;
|
|
29
|
+
border-bottom: 1px solid #2b2c2c;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.header {
|
|
33
|
+
position: fixed;
|
|
34
|
+
width: -webkit-fill-available;
|
|
35
|
+
width: -moz-available;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
background: rgba(0,0,0,0);
|
|
38
|
+
backdrop-filter: blur(10px);
|
|
39
|
+
border-bottom: 1px solid #191a1a;
|
|
40
|
+
transition: all 0.2s linear;
|
|
41
|
+
/* box-shadow: 0px -9px 12px 8px #888888; */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.border-border {
|
|
45
|
+
border-color: #ece6e70d !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
textarea {
|
|
49
|
+
color: #e8e8e3;
|
|
50
|
+
font-weight: 200;
|
|
51
|
+
background-color: #1f2121;
|
|
52
|
+
padding-top: 10px;
|
|
53
|
+
border-top-right-radius: 16px;
|
|
54
|
+
border-top-left-radius: 16px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.text-default {
|
|
58
|
+
color: '#e8e8e3'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.color-accent {
|
|
62
|
+
color: #94ccff !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.bg-sidebar {
|
|
66
|
+
background: #1f2121 !important;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.text-center {
|
|
70
|
+
text-align: center;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sidebar-list {
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
line-height: 1.7;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.card-preview {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
background: #1f2121;
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
border: 1px solid #ece6e70d;
|
|
84
|
+
color: #fff;
|
|
85
|
+
padding: 9px;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
transition: all 0.25s linear;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.card-preview:hover {
|
|
91
|
+
border: 1px solid #666;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.card-preview h5 {
|
|
95
|
+
font-size: 14px;
|
|
96
|
+
margin-top: 6px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.card-preview p {
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
font-weight: 100;
|
|
102
|
+
line-height: 1.7;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.card-preview hr {
|
|
106
|
+
border-color: #ece6e70d;
|
|
107
|
+
margin-top: 16px;
|
|
108
|
+
margin-bottom: 8px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.card-preview small {
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
font-weight: 200;
|
|
114
|
+
color: #999;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.thread-preview {
|
|
118
|
+
height: 256px;
|
|
119
|
+
transition: all 0.25s ease-in-out;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.markdown {
|
|
124
|
+
width: 100%;
|
|
125
|
+
padding: 8rem 14rem;
|
|
126
|
+
color: #fff;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.markdown p {
|
|
130
|
+
margin-bottom: 24px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.markdown li {
|
|
134
|
+
margin-bottom: 24px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.markdown small {
|
|
138
|
+
opacity: 0.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.message-user {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: row-reverse;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.message-user p {
|
|
147
|
+
background: #333;
|
|
148
|
+
padding: 16px;
|
|
149
|
+
border-radius: 12px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.effect-shine {
|
|
153
|
+
mask-image: linear-gradient(-75deg, rgba(0,0,0,.6) 30%, #000 50%, rgba(0,0,0,.6) 70%);
|
|
154
|
+
-webkit-mask-image: linear-gradient(-75deg, rgba(0,0,0,.6) 30%, #000 50%, rgba(0,0,0,.6) 70%);
|
|
155
|
+
mask-size: 200%;
|
|
156
|
+
-webkit-mask-size: 200%;
|
|
157
|
+
animation: shine 2s infinite;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@-webkit-keyframes shine {
|
|
161
|
+
from {
|
|
162
|
+
-webkit-mask-position: 150%;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
to {
|
|
166
|
+
-webkit-mask-position: -50%;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@keyframes shine {
|
|
171
|
+
from {
|
|
172
|
+
mask-position: 150%;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
to {
|
|
176
|
+
mask-position: -50%;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
180
|
+
</head>
|
|
181
|
+
|
|
182
|
+
<body style="background: #191a1a;">
|
|
183
|
+
<div id="root" style="background: #191a1a;"></div>
|
|
184
|
+
|
|
185
|
+
{{>imports}}
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|