@pantheon.ai/mcp 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.
@@ -0,0 +1,265 @@
1
+ import type { ApiExecutorContext } from "@pantheon/sdk/lib/api-executor"
2
+ import { StreamResponseParser } from "@pantheon/sdk/lib/stream-parser"
3
+ import { isValidator, type Validator } from "@pantheon/sdk/lib/validator"
4
+ import { parseTemplate, type PrimitiveValue, type Template } from "url-template"
5
+ import type { ZodType } from "zod"
6
+
7
+ export type ApiMethod = "get" | "post" | "put" | "delete" | "patch"
8
+
9
+ export type PhantomType<T> = {
10
+ readonly __phantom_type__: T
11
+ }
12
+
13
+ export function typeOf<T>(): PhantomType<T> {
14
+ return null as never as PhantomType<T>
15
+ }
16
+
17
+ type ParamsTypeOf<Params extends readonly string[]> = Params extends []
18
+ ? null
19
+ : {
20
+ [K in Params[number] as K extends `${infer Name}?` ? Name : never]?:
21
+ | PrimitiveValue
22
+ | PrimitiveValue[]
23
+ } & {
24
+ [K in Params[number] as K extends `${string}?` ? never : K]:
25
+ | PrimitiveValue
26
+ | PrimitiveValue[]
27
+ }
28
+
29
+ declare global {
30
+ interface RequestInit {
31
+ duplex?: "half"
32
+ }
33
+ }
34
+
35
+ export interface Api<
36
+ Method extends ApiMethod,
37
+ Params extends Record<string, PrimitiveValue | PrimitiveValue[]> | null,
38
+ RequestBody,
39
+ ResponseBody,
40
+ > {
41
+ readonly method: Method
42
+ readonly signature: string
43
+ readonly url: Params extends null ? () => string : (params: Params) => string
44
+ readonly requestInit: Method extends "get" | "delete"
45
+ ? () => Pick<RequestInit, "headers" | "duplex" | "method" | "body">
46
+ : (
47
+ body: RequestBody
48
+ ) => Pick<RequestInit, "headers" | "duplex" | "method" | "body">
49
+ readonly handleResponse: (
50
+ response: Response,
51
+ context: ApiExecutorContext
52
+ ) => Promise<ResponseBody>
53
+ }
54
+
55
+ export type ApiParams<API> = API extends Api<any, infer P, any, any> ? P : never
56
+ export type ApiRequestBody<API> =
57
+ API extends Api<any, any, infer B, any> ? B : never
58
+ export type ApiResponseBody<API> =
59
+ API extends Api<any, any, any, infer R> ? R : never
60
+
61
+ interface UrlTemplate<
62
+ Method extends ApiMethod,
63
+ Params extends Record<string, PrimitiveValue | PrimitiveValue[]> | null,
64
+ > {
65
+ readonly method: Method
66
+ readonly templateUrl: string
67
+ readonly signature: string
68
+ readonly template: Template
69
+ readonly __params__: PhantomType<Params>
70
+ }
71
+
72
+ function createUrlTemplate<Method extends ApiMethod>(method: Method) {
73
+ return function <Params extends string[]>(
74
+ arr: TemplateStringsArray,
75
+ ...params: Params
76
+ ): UrlTemplate<Method, ParamsTypeOf<Params>> {
77
+ let url = arr[0]
78
+ let expandedPaths = false
79
+ for (let i = 1; i < arr.length; i++) {
80
+ if (params[i - 1] === "path*") {
81
+ url += "<path*>"
82
+ expandedPaths = true
83
+ } else {
84
+ url += "{" + params[i - 1].replace(/\?$/, "") + "}" + arr[i]
85
+ }
86
+ }
87
+
88
+ const [pathPart, searchPart] = url.split("?")
89
+
90
+ const pathPartTemplate: Template = expandedPaths
91
+ ? {
92
+ expand: (context) => {
93
+ return parseTemplate(
94
+ pathPart.replace(
95
+ /<path\*>/,
96
+ String(context["path*"]).replace(/^\//, "")
97
+ )
98
+ ).expand(context)
99
+ },
100
+ }
101
+ : parseTemplate(pathPart)
102
+
103
+ // key={template_value}, this will allow array values.
104
+ // key=prefix-{value}, this will go through the normal template expansion.
105
+ // key=constant, will be added as is.
106
+ const searchParamsBuilders: Array<
107
+ (usp: URLSearchParams, context: Record<string, any>) => void
108
+ > = []
109
+
110
+ if (searchPart) {
111
+ searchPart.split("&").forEach((entry) => {
112
+ const [key, value = ""] = entry.split("=").map(decodeURIComponent)
113
+
114
+ if (/^\{[^}]+}$/.test(value)) {
115
+ const templateValue = value.slice(1, -1)
116
+ searchParamsBuilders.push((usp, context) => {
117
+ if (templateValue in context) {
118
+ const value = context[templateValue]
119
+ if (value === undefined) {
120
+ return
121
+ }
122
+ if (Array.isArray(value)) {
123
+ value.forEach((v) => usp.append(key, String(v)))
124
+ } else {
125
+ usp.append(key, String(value))
126
+ }
127
+ }
128
+ })
129
+ } else if (value !== "") {
130
+ const template = parseTemplate(value)
131
+
132
+ searchParamsBuilders.push((usp, context) => {
133
+ usp.append(key, template.expand(context))
134
+ })
135
+ } else {
136
+ searchParamsBuilders.push((usp) => {
137
+ usp.append(key, "")
138
+ })
139
+ }
140
+ })
141
+ }
142
+
143
+ return {
144
+ method,
145
+ signature: `${method} ${url}`,
146
+ templateUrl: url,
147
+ template: {
148
+ expand: (context) => {
149
+ const pathPart = pathPartTemplate.expand(context)
150
+ const usp = new URLSearchParams()
151
+ searchParamsBuilders.forEach((template) => {
152
+ template(usp, context)
153
+ })
154
+
155
+ if (Array.from(usp).length > 0) {
156
+ return pathPart + "?" + usp.toString()
157
+ } else {
158
+ return pathPart
159
+ }
160
+ },
161
+ },
162
+ __params__: typeOf<ParamsTypeOf<Params>>(),
163
+ }
164
+ }
165
+ }
166
+
167
+ export const get = createUrlTemplate("get")
168
+ export const post = createUrlTemplate("post")
169
+ export const put = createUrlTemplate("put")
170
+ export const del = createUrlTemplate("delete")
171
+ export const patch = createUrlTemplate("patch")
172
+
173
+ type ParamsOf<T extends UrlTemplate<any, any>> = [T] extends [
174
+ UrlTemplate<any, infer O>,
175
+ ]
176
+ ? O
177
+ : null
178
+ type MethodOf<T extends UrlTemplate<any, any>> = [T] extends [
179
+ UrlTemplate<infer O, any>,
180
+ ]
181
+ ? O
182
+ : never
183
+
184
+ type ResponseSchemaType =
185
+ | ZodType<any>
186
+ | StreamResponseParser<any>
187
+ | Validator<any>
188
+ | "raw"
189
+ type inferResponse<ResponseSchema extends ResponseSchemaType> =
190
+ ResponseSchema extends Validator<infer O>
191
+ ? O
192
+ : ResponseSchema extends ZodType<infer O>
193
+ ? O
194
+ : ResponseSchema extends StreamResponseParser<infer O>
195
+ ? ReadableStream<O>
196
+ : ResponseSchema extends "raw"
197
+ ? Response
198
+ : never
199
+
200
+ export function defineApi<
201
+ UrlTemplateParameter extends UrlTemplate<any, any>,
202
+ RequestBody = void,
203
+ ResponseSchema extends ResponseSchemaType = "raw",
204
+ >(
205
+ urlTemplate: UrlTemplateParameter,
206
+ _type: MethodOf<UrlTemplateParameter> extends "get" | "delete"
207
+ ? null
208
+ : PhantomType<RequestBody> | null,
209
+ responseSchema: ResponseSchema
210
+ ) {
211
+ type Method = MethodOf<UrlTemplateParameter>
212
+ type Params = ParamsOf<UrlTemplateParameter>
213
+ type ResponseBody = inferResponse<ResponseSchema>
214
+ const method = urlTemplate.method
215
+
216
+ const api: Api<Method, Params, RequestBody, ResponseBody> = {
217
+ method: urlTemplate.method,
218
+ signature: urlTemplate.signature,
219
+ url: ((params: any | null) =>
220
+ urlTemplate.template.expand(params ?? {})) as any,
221
+ requestInit: (
222
+ body?: any
223
+ ): Pick<RequestInit, "headers" | "duplex" | "method" | "body"> => {
224
+ if (method === "get" || method === "delete") {
225
+ return {
226
+ method,
227
+ }
228
+ }
229
+
230
+ if (body instanceof ReadableStream) {
231
+ return {
232
+ method,
233
+ body,
234
+ duplex: "half",
235
+ }
236
+ } else if (body instanceof FormData) {
237
+ return {
238
+ method,
239
+ body,
240
+ }
241
+ } else {
242
+ return {
243
+ method,
244
+ body: JSON.stringify(body),
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ },
248
+ }
249
+ }
250
+ },
251
+ handleResponse: async (response, context) => {
252
+ if (responseSchema instanceof StreamResponseParser) {
253
+ return responseSchema.handleResponse(response, context)
254
+ } else if (responseSchema === "raw") {
255
+ return response
256
+ } else if (isValidator(responseSchema)) {
257
+ return responseSchema.parse(response)
258
+ } else {
259
+ return responseSchema.parse(await response.json())
260
+ }
261
+ },
262
+ }
263
+
264
+ return api
265
+ }
@@ -0,0 +1,30 @@
1
+ import type { ApiExecutorContext } from "@pantheon/sdk/lib/api-executor"
2
+
3
+ export interface StreamResponseParserOptions<Output> {
4
+ readonly validateResponse?: (response: Response) => void | Promise<void>
5
+ readonly pipe: (
6
+ input: ReadableStream<BufferSource>,
7
+ context: ApiExecutorContext & { readonly response: Response }
8
+ ) => ReadableStream<Output>
9
+ }
10
+
11
+ export class StreamResponseParser<Output> {
12
+ private readonly options: StreamResponseParserOptions<Output>
13
+
14
+ constructor(options: StreamResponseParserOptions<Output>) {
15
+ this.options = options
16
+ }
17
+
18
+ async handleResponse(response: Response, context: ApiExecutorContext) {
19
+ this.options.validateResponse?.(response)
20
+
21
+ if (!response.body) {
22
+ throw new Error("Response body is missing")
23
+ }
24
+
25
+ return this.options.pipe(
26
+ response.body,
27
+ Object.freeze({ ...context, response })
28
+ )
29
+ }
30
+ }
@@ -0,0 +1,19 @@
1
+ import type { PhantomType } from "@pantheon/sdk/lib/api"
2
+
3
+ export const VALIDATOR_SYMBOL = Symbol("validator")
4
+
5
+ export interface Validator<Output> {
6
+ readonly [VALIDATOR_SYMBOL]: true
7
+ readonly __phantom__: PhantomType<Output>
8
+
9
+ parse(input: Response): Output | Promise<Output>
10
+ }
11
+
12
+ export function isValidator(object: any): object is Validator<unknown> {
13
+ return (
14
+ typeof object === "object" &&
15
+ object !== null &&
16
+ object[VALIDATOR_SYMBOL] === true &&
17
+ typeof object.parse === "function"
18
+ )
19
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ createStreamingUIMessageState,
3
+ processUIMessageStream,
4
+ type StreamingUIMessageState,
5
+ type UIMessage,
6
+ type UIMessageChunk,
7
+ } from "ai"
8
+
9
+ export async function fastReadUIMessageStream<UI_MESSAGE extends UIMessage>({
10
+ stream,
11
+ messageId,
12
+ onState,
13
+ state: propState,
14
+ }: {
15
+ stream: ReadableStream<UIMessageChunk>
16
+ messageId: string
17
+ onState: (state: StreamingUIMessageState<UI_MESSAGE>) => void
18
+ state?: StreamingUIMessageState<UI_MESSAGE>
19
+ }) {
20
+ const state =
21
+ propState ??
22
+ createStreamingUIMessageState<UI_MESSAGE>({
23
+ lastMessage: undefined,
24
+ messageId,
25
+ })
26
+
27
+ const res = processUIMessageStream<UI_MESSAGE>({
28
+ stream,
29
+ runUpdateMessageJob(
30
+ job: (options: {
31
+ state: StreamingUIMessageState<UI_MESSAGE>
32
+ write: () => void
33
+ }) => Promise<void>
34
+ ) {
35
+ return job({
36
+ state,
37
+ write: () => {},
38
+ })
39
+ },
40
+ onError: (error) => {
41
+ // ignore error chunk
42
+ state.message.metadata ??= {}
43
+ ;(state.message.metadata as any).__stream_error__ = (
44
+ error as Error
45
+ ).message
46
+ },
47
+ })
48
+
49
+ let chunks = 0
50
+
51
+ const reader = res.getReader()
52
+ while (true) {
53
+ const chunk = await reader.read()
54
+
55
+ if (chunk.done) break
56
+ chunks++
57
+ onState(state)
58
+ }
59
+
60
+ return { chunks, state }
61
+ }
62
+
63
+ export type UIMessageStreamCursor<UI_MESSAGE extends UIMessage> = {
64
+ offset: number
65
+ state: StreamingUIMessageState<UI_MESSAGE>
66
+ chunk: UIMessageChunk
67
+ }
68
+
69
+ export function fastResumeReadUIMessageStream<UI_MESSAGE extends UIMessage>({
70
+ stream,
71
+ messageId,
72
+ state: propState,
73
+ offset = 0,
74
+ }: {
75
+ stream: ReadableStream<UIMessageChunk>
76
+ messageId: string
77
+ state?: StreamingUIMessageState<UI_MESSAGE>
78
+ offset?: number
79
+ }) {
80
+ const state =
81
+ propState ??
82
+ createStreamingUIMessageState<UI_MESSAGE>({
83
+ lastMessage: undefined,
84
+ messageId,
85
+ })
86
+
87
+ return new ReadableStream<UIMessageStreamCursor<UI_MESSAGE>>({
88
+ async start(controller) {
89
+ try {
90
+ const res = processUIMessageStream<UI_MESSAGE>({
91
+ stream,
92
+ runUpdateMessageJob(
93
+ job: (options: {
94
+ state: StreamingUIMessageState<UI_MESSAGE>
95
+ write: () => void
96
+ }) => Promise<void>
97
+ ) {
98
+ return job({
99
+ state,
100
+ write: () => {},
101
+ })
102
+ },
103
+ onError: (error) => {
104
+ // ignore error chunk
105
+ state.message.metadata ??= {}
106
+ ;(state.message.metadata as any).__stream_error__ = (
107
+ error as Error
108
+ ).message
109
+ },
110
+ })
111
+
112
+ let chunks = 0
113
+
114
+ const reader = res.getReader()
115
+
116
+ while (true) {
117
+ const chunk = await reader.read()
118
+ if (chunk.done) break
119
+ controller.enqueue({
120
+ offset: chunks + offset,
121
+ state,
122
+ chunk: chunk.value,
123
+ })
124
+ chunks++
125
+ }
126
+
127
+ controller.close()
128
+ } catch (e) {
129
+ controller.error(e)
130
+ }
131
+ },
132
+ })
133
+ }
@@ -0,0 +1,32 @@
1
+ import { z, type ZodType } from "zod"
2
+
3
+ export interface ZodStreamOptions<Input, Z extends ZodType<unknown, Input>> {
4
+ schema: Z
5
+ onParseFailed?: (
6
+ error: unknown,
7
+ chunk: unknown,
8
+ controller: TransformStreamDefaultController<z.infer<Z>>
9
+ ) => void
10
+ }
11
+
12
+ export class ZodStream<
13
+ Input,
14
+ Z extends ZodType<unknown, Input>,
15
+ > extends TransformStream<Input, z.infer<Z>> {
16
+ constructor({ schema, onParseFailed }: ZodStreamOptions<Input, Z>) {
17
+ super({
18
+ async transform(chunk, controller) {
19
+ const result = await schema.safeParseAsync(chunk)
20
+ if (result.success) {
21
+ controller.enqueue(result.data)
22
+ } else if (onParseFailed) {
23
+ onParseFailed(result.error, chunk, controller)
24
+ } else {
25
+ controller.error(result.error)
26
+ }
27
+ },
28
+ })
29
+ }
30
+ }
31
+
32
+ export const zodJsonDate = z.string() // z.iso.datetime().transform((d) => new Date(d));