utils.go

  1package mcp
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6)
  7
  8// ClientRequest types
  9var _ ClientRequest = &PingRequest{}
 10var _ ClientRequest = &InitializeRequest{}
 11var _ ClientRequest = &CompleteRequest{}
 12var _ ClientRequest = &SetLevelRequest{}
 13var _ ClientRequest = &GetPromptRequest{}
 14var _ ClientRequest = &ListPromptsRequest{}
 15var _ ClientRequest = &ListResourcesRequest{}
 16var _ ClientRequest = &ReadResourceRequest{}
 17var _ ClientRequest = &SubscribeRequest{}
 18var _ ClientRequest = &UnsubscribeRequest{}
 19var _ ClientRequest = &CallToolRequest{}
 20var _ ClientRequest = &ListToolsRequest{}
 21
 22// ClientNotification types
 23var _ ClientNotification = &CancelledNotification{}
 24var _ ClientNotification = &ProgressNotification{}
 25var _ ClientNotification = &InitializedNotification{}
 26var _ ClientNotification = &RootsListChangedNotification{}
 27
 28// ClientResult types
 29var _ ClientResult = &EmptyResult{}
 30var _ ClientResult = &CreateMessageResult{}
 31var _ ClientResult = &ListRootsResult{}
 32
 33// ServerRequest types
 34var _ ServerRequest = &PingRequest{}
 35var _ ServerRequest = &CreateMessageRequest{}
 36var _ ServerRequest = &ListRootsRequest{}
 37
 38// ServerNotification types
 39var _ ServerNotification = &CancelledNotification{}
 40var _ ServerNotification = &ProgressNotification{}
 41var _ ServerNotification = &LoggingMessageNotification{}
 42var _ ServerNotification = &ResourceUpdatedNotification{}
 43var _ ServerNotification = &ResourceListChangedNotification{}
 44var _ ServerNotification = &ToolListChangedNotification{}
 45var _ ServerNotification = &PromptListChangedNotification{}
 46
 47// ServerResult types
 48var _ ServerResult = &EmptyResult{}
 49var _ ServerResult = &InitializeResult{}
 50var _ ServerResult = &CompleteResult{}
 51var _ ServerResult = &GetPromptResult{}
 52var _ ServerResult = &ListPromptsResult{}
 53var _ ServerResult = &ListResourcesResult{}
 54var _ ServerResult = &ReadResourceResult{}
 55var _ ServerResult = &CallToolResult{}
 56var _ ServerResult = &ListToolsResult{}
 57
 58// Helper functions for type assertions
 59
 60// asType attempts to cast the given interface to the given type
 61func asType[T any](content interface{}) (*T, bool) {
 62	tc, ok := content.(T)
 63	if !ok {
 64		return nil, false
 65	}
 66	return &tc, true
 67}
 68
 69// AsTextContent attempts to cast the given interface to TextContent
 70func AsTextContent(content interface{}) (*TextContent, bool) {
 71	return asType[TextContent](content)
 72}
 73
 74// AsImageContent attempts to cast the given interface to ImageContent
 75func AsImageContent(content interface{}) (*ImageContent, bool) {
 76	return asType[ImageContent](content)
 77}
 78
 79// AsEmbeddedResource attempts to cast the given interface to EmbeddedResource
 80func AsEmbeddedResource(content interface{}) (*EmbeddedResource, bool) {
 81	return asType[EmbeddedResource](content)
 82}
 83
 84// AsTextResourceContents attempts to cast the given interface to TextResourceContents
 85func AsTextResourceContents(content interface{}) (*TextResourceContents, bool) {
 86	return asType[TextResourceContents](content)
 87}
 88
 89// AsBlobResourceContents attempts to cast the given interface to BlobResourceContents
 90func AsBlobResourceContents(content interface{}) (*BlobResourceContents, bool) {
 91	return asType[BlobResourceContents](content)
 92}
 93
 94// Helper function for JSON-RPC
 95
 96// NewJSONRPCResponse creates a new JSONRPCResponse with the given id and result
 97func NewJSONRPCResponse(id RequestId, result Result) JSONRPCResponse {
 98	return JSONRPCResponse{
 99		JSONRPC: JSONRPC_VERSION,
100		ID:      id,
101		Result:  result,
102	}
103}
104
105// NewJSONRPCError creates a new JSONRPCResponse with the given id, code, and message
106func NewJSONRPCError(
107	id RequestId,
108	code int,
109	message string,
110	data interface{},
111) JSONRPCError {
112	return JSONRPCError{
113		JSONRPC: JSONRPC_VERSION,
114		ID:      id,
115		Error: struct {
116			Code    int         `json:"code"`
117			Message string      `json:"message"`
118			Data    interface{} `json:"data,omitempty"`
119		}{
120			Code:    code,
121			Message: message,
122			Data:    data,
123		},
124	}
125}
126
127// Helper function for creating a progress notification
128func NewProgressNotification(
129	token ProgressToken,
130	progress float64,
131	total *float64,
132) ProgressNotification {
133	notification := ProgressNotification{
134		Notification: Notification{
135			Method: "notifications/progress",
136		},
137		Params: struct {
138			ProgressToken ProgressToken `json:"progressToken"`
139			Progress      float64       `json:"progress"`
140			Total         float64       `json:"total,omitempty"`
141		}{
142			ProgressToken: token,
143			Progress:      progress,
144		},
145	}
146	if total != nil {
147		notification.Params.Total = *total
148	}
149	return notification
150}
151
152// Helper function for creating a logging message notification
153func NewLoggingMessageNotification(
154	level LoggingLevel,
155	logger string,
156	data interface{},
157) LoggingMessageNotification {
158	return LoggingMessageNotification{
159		Notification: Notification{
160			Method: "notifications/message",
161		},
162		Params: struct {
163			Level  LoggingLevel `json:"level"`
164			Logger string       `json:"logger,omitempty"`
165			Data   interface{}  `json:"data"`
166		}{
167			Level:  level,
168			Logger: logger,
169			Data:   data,
170		},
171	}
172}
173
174// Helper function to create a new PromptMessage
175func NewPromptMessage(role Role, content Content) PromptMessage {
176	return PromptMessage{
177		Role:    role,
178		Content: content,
179	}
180}
181
182// Helper function to create a new TextContent
183func NewTextContent(text string) TextContent {
184	return TextContent{
185		Type: "text",
186		Text: text,
187	}
188}
189
190// Helper function to create a new ImageContent
191func NewImageContent(data, mimeType string) ImageContent {
192	return ImageContent{
193		Type:     "image",
194		Data:     data,
195		MIMEType: mimeType,
196	}
197}
198
199// Helper function to create a new EmbeddedResource
200func NewEmbeddedResource(resource ResourceContents) EmbeddedResource {
201	return EmbeddedResource{
202		Type:     "resource",
203		Resource: resource,
204	}
205}
206
207// NewToolResultText creates a new CallToolResult with a text content
208func NewToolResultText(text string) *CallToolResult {
209	return &CallToolResult{
210		Content: []Content{
211			TextContent{
212				Type: "text",
213				Text: text,
214			},
215		},
216	}
217}
218
219// NewToolResultImage creates a new CallToolResult with both text and image content
220func NewToolResultImage(text, imageData, mimeType string) *CallToolResult {
221	return &CallToolResult{
222		Content: []Content{
223			TextContent{
224				Type: "text",
225				Text: text,
226			},
227			ImageContent{
228				Type:     "image",
229				Data:     imageData,
230				MIMEType: mimeType,
231			},
232		},
233	}
234}
235
236// NewToolResultResource creates a new CallToolResult with an embedded resource
237func NewToolResultResource(
238	text string,
239	resource ResourceContents,
240) *CallToolResult {
241	return &CallToolResult{
242		Content: []Content{
243			TextContent{
244				Type: "text",
245				Text: text,
246			},
247			EmbeddedResource{
248				Type:     "resource",
249				Resource: resource,
250			},
251		},
252	}
253}
254
255// NewListResourcesResult creates a new ListResourcesResult
256func NewListResourcesResult(
257	resources []Resource,
258	nextCursor Cursor,
259) *ListResourcesResult {
260	return &ListResourcesResult{
261		PaginatedResult: PaginatedResult{
262			NextCursor: nextCursor,
263		},
264		Resources: resources,
265	}
266}
267
268// NewListResourceTemplatesResult creates a new ListResourceTemplatesResult
269func NewListResourceTemplatesResult(
270	templates []ResourceTemplate,
271	nextCursor Cursor,
272) *ListResourceTemplatesResult {
273	return &ListResourceTemplatesResult{
274		PaginatedResult: PaginatedResult{
275			NextCursor: nextCursor,
276		},
277		ResourceTemplates: templates,
278	}
279}
280
281// NewReadResourceResult creates a new ReadResourceResult with text content
282func NewReadResourceResult(text string) *ReadResourceResult {
283	return &ReadResourceResult{
284		Contents: []ResourceContents{
285			TextResourceContents{
286				Text: text,
287			},
288		},
289	}
290}
291
292// NewListPromptsResult creates a new ListPromptsResult
293func NewListPromptsResult(
294	prompts []Prompt,
295	nextCursor Cursor,
296) *ListPromptsResult {
297	return &ListPromptsResult{
298		PaginatedResult: PaginatedResult{
299			NextCursor: nextCursor,
300		},
301		Prompts: prompts,
302	}
303}
304
305// NewGetPromptResult creates a new GetPromptResult
306func NewGetPromptResult(
307	description string,
308	messages []PromptMessage,
309) *GetPromptResult {
310	return &GetPromptResult{
311		Description: description,
312		Messages:    messages,
313	}
314}
315
316// NewListToolsResult creates a new ListToolsResult
317func NewListToolsResult(tools []Tool, nextCursor Cursor) *ListToolsResult {
318	return &ListToolsResult{
319		PaginatedResult: PaginatedResult{
320			NextCursor: nextCursor,
321		},
322		Tools: tools,
323	}
324}
325
326// NewInitializeResult creates a new InitializeResult
327func NewInitializeResult(
328	protocolVersion string,
329	capabilities ServerCapabilities,
330	serverInfo Implementation,
331	instructions string,
332) *InitializeResult {
333	return &InitializeResult{
334		ProtocolVersion: protocolVersion,
335		Capabilities:    capabilities,
336		ServerInfo:      serverInfo,
337		Instructions:    instructions,
338	}
339}
340
341// Helper for formatting numbers in tool results
342func FormatNumberResult(value float64) *CallToolResult {
343	return NewToolResultText(fmt.Sprintf("%.2f", value))
344}
345
346func ExtractString(data map[string]any, key string) string {
347	if value, ok := data[key]; ok {
348		if str, ok := value.(string); ok {
349			return str
350		}
351	}
352	return ""
353}
354
355func ExtractMap(data map[string]any, key string) map[string]any {
356	if value, ok := data[key]; ok {
357		if m, ok := value.(map[string]any); ok {
358			return m
359		}
360	}
361	return nil
362}
363
364func ParseContent(contentMap map[string]any) (Content, error) {
365	contentType := ExtractString(contentMap, "type")
366
367	switch contentType {
368	case "text":
369		text := ExtractString(contentMap, "text")
370		if text == "" {
371			return nil, fmt.Errorf("text is missing")
372		}
373		return NewTextContent(text), nil
374
375	case "image":
376		data := ExtractString(contentMap, "data")
377		mimeType := ExtractString(contentMap, "mimeType")
378		if data == "" || mimeType == "" {
379			return nil, fmt.Errorf("image data or mimeType is missing")
380		}
381		return NewImageContent(data, mimeType), nil
382
383	case "resource":
384		resourceMap := ExtractMap(contentMap, "resource")
385		if resourceMap == nil {
386			return nil, fmt.Errorf("resource is missing")
387		}
388
389		resourceContents, err := ParseResourceContents(resourceMap)
390		if err != nil {
391			return nil, err
392		}
393
394		return NewEmbeddedResource(resourceContents), nil
395	}
396
397	return nil, fmt.Errorf("unsupported content type: %s", contentType)
398}
399
400func ParseGetPromptResult(rawMessage *json.RawMessage) (*GetPromptResult, error) {
401	var jsonContent map[string]any
402	if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
403		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
404	}
405
406	result := GetPromptResult{}
407
408	meta, ok := jsonContent["_meta"]
409	if ok {
410		if metaMap, ok := meta.(map[string]any); ok {
411			result.Meta = metaMap
412		}
413	}
414
415	description, ok := jsonContent["description"]
416	if ok {
417		if descriptionStr, ok := description.(string); ok {
418			result.Description = descriptionStr
419		}
420	}
421
422	messages, ok := jsonContent["messages"]
423	if ok {
424		messagesArr, ok := messages.([]any)
425		if !ok {
426			return nil, fmt.Errorf("messages is not an array")
427		}
428
429		for _, message := range messagesArr {
430			messageMap, ok := message.(map[string]any)
431			if !ok {
432				return nil, fmt.Errorf("message is not an object")
433			}
434
435			// Extract role
436			roleStr := ExtractString(messageMap, "role")
437			if roleStr == "" || (roleStr != string(RoleAssistant) && roleStr != string(RoleUser)) {
438				return nil, fmt.Errorf("unsupported role: %s", roleStr)
439			}
440
441			// Extract content
442			contentMap, ok := messageMap["content"].(map[string]any)
443			if !ok {
444				return nil, fmt.Errorf("content is not an object")
445			}
446
447			// Process content
448			content, err := ParseContent(contentMap)
449			if err != nil {
450				return nil, err
451			}
452
453			// Append processed message
454			result.Messages = append(result.Messages, NewPromptMessage(Role(roleStr), content))
455
456		}
457	}
458
459	return &result, nil
460}
461
462func ParseCallToolResult(rawMessage *json.RawMessage) (*CallToolResult, error) {
463	var jsonContent map[string]any
464	if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
465		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
466	}
467
468	var result CallToolResult
469
470	meta, ok := jsonContent["_meta"]
471	if ok {
472		if metaMap, ok := meta.(map[string]any); ok {
473			result.Meta = metaMap
474		}
475	}
476
477	isError, ok := jsonContent["isError"]
478	if ok {
479		if isErrorBool, ok := isError.(bool); ok {
480			result.IsError = isErrorBool
481		}
482	}
483
484	contents, ok := jsonContent["content"]
485	if !ok {
486		return nil, fmt.Errorf("content is missing")
487	}
488
489	contentArr, ok := contents.([]any)
490	if !ok {
491		return nil, fmt.Errorf("content is not an array")
492	}
493
494	for _, content := range contentArr {
495		// Extract content
496		contentMap, ok := content.(map[string]any)
497		if !ok {
498			return nil, fmt.Errorf("content is not an object")
499		}
500
501		// Process content
502		content, err := ParseContent(contentMap)
503		if err != nil {
504			return nil, err
505		}
506
507		result.Content = append(result.Content, content)
508	}
509
510	return &result, nil
511}
512
513func ParseResourceContents(contentMap map[string]any) (ResourceContents, error) {
514	uri := ExtractString(contentMap, "uri")
515	if uri == "" {
516		return nil, fmt.Errorf("resource uri is missing")
517	}
518
519	mimeType := ExtractString(contentMap, "mimeType")
520
521	if text := ExtractString(contentMap, "text"); text != "" {
522		return TextResourceContents{
523			URI:      uri,
524			MIMEType: mimeType,
525			Text:     text,
526		}, nil
527	}
528
529	if blob := ExtractString(contentMap, "blob"); blob != "" {
530		return BlobResourceContents{
531			URI:      uri,
532			MIMEType: mimeType,
533			Blob:     blob,
534		}, nil
535	}
536
537	return nil, fmt.Errorf("unsupported resource type")
538}
539
540func ParseReadResourceResult(rawMessage *json.RawMessage) (*ReadResourceResult, error) {
541	var jsonContent map[string]any
542	if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil {
543		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
544	}
545
546	var result ReadResourceResult
547
548	meta, ok := jsonContent["_meta"]
549	if ok {
550		if metaMap, ok := meta.(map[string]any); ok {
551			result.Meta = metaMap
552		}
553	}
554
555	contents, ok := jsonContent["contents"]
556	if !ok {
557		return nil, fmt.Errorf("contents is missing")
558	}
559
560	contentArr, ok := contents.([]any)
561	if !ok {
562		return nil, fmt.Errorf("contents is not an array")
563	}
564
565	for _, content := range contentArr {
566		// Extract content
567		contentMap, ok := content.(map[string]any)
568		if !ok {
569			return nil, fmt.Errorf("content is not an object")
570		}
571
572		// Process content
573		content, err := ParseResourceContents(contentMap)
574		if err != nil {
575			return nil, err
576		}
577
578		result.Contents = append(result.Contents, content)
579	}
580
581	return &result, nil
582}