diff --git a/go.work b/go.work index cb992ba..7ad3362 100644 --- a/go.work +++ b/go.work @@ -9,6 +9,7 @@ use ( ./servlets/github ./servlets/google-maps-image ./servlets/historical-flight-api + ./servlets/onedrive ./servlets/tenor-gifs ./servlets/trello ./servlets/wordpress diff --git a/servlets/onedrive/.gitignore b/servlets/onedrive/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/servlets/onedrive/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/servlets/onedrive/go.mod b/servlets/onedrive/go.mod new file mode 100644 index 0000000..ba0424f --- /dev/null +++ b/servlets/onedrive/go.mod @@ -0,0 +1,5 @@ +module onedrive + +go 1.22.1 + +require github.com/extism/go-pdk v1.1.0 diff --git a/servlets/onedrive/go.sum b/servlets/onedrive/go.sum new file mode 100644 index 0000000..e0fb44c --- /dev/null +++ b/servlets/onedrive/go.sum @@ -0,0 +1,2 @@ +github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY= +github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= diff --git a/servlets/onedrive/main.go b/servlets/onedrive/main.go new file mode 100644 index 0000000..7fcadb0 --- /dev/null +++ b/servlets/onedrive/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "fmt" + + "github.com/extism/go-pdk" +) + +// Tool definitions +var ( + ListDriveItemsTool = ToolDescription{ + Name: "list-drive-items", + Description: "List items in the root of a drive", + InputSchema: schema{ + "type": "object", + "properties": props{ + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "top": prop("integer", "Number of items to return (max 999)"), + "orderby": prop("string", "Property to sort by (e.g., name, lastModifiedDateTime)"), + }, + }, + } + + ListRecentFilesTool = ToolDescription{ + Name: "recent-files", + Description: "Get a list of recently accessed files", + InputSchema: schema{ + "type": "object", + "properties": props{ + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "top": prop("integer", "Number of items to return (max 999)"), + }, + }, + } + + ListSharedWithMeTool = ToolDescription{ + Name: "shared-with-me", + Description: "List files and folders that have been shared with you", + InputSchema: schema{ + "type": "object", + "properties": props{ + "top": prop("integer", "Number of items to return (max 999)"), + }, + }, + } + + SearchDriveTool = ToolDescription{ + Name: "search", + Description: "Search for files and folders in a drive", + InputSchema: schema{ + "type": "object", + "properties": props{ + "query": prop("string", "Search query string"), + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "top": prop("integer", "Number of items to return (max 999)"), + }, + "required": []string{"query"}, + }, + } + + CreateFolderTool = ToolDescription{ + Name: "create-folder", + Description: "Create a new folder in a drive", + InputSchema: schema{ + "type": "object", + "properties": props{ + "name": prop("string", "The name of the folder to create"), + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "parent": prop("string", "The parent folder ID (optional, defaults to root)"), + }, + "required": []string{"name"}, + }, + } + + ListChildrenOfDriveTool = ToolDescription{ + Name: "list-drive-children", + Description: "List all drives available to the user", + InputSchema: schema{ + "type": "object", + "properties": props{}, + }, + } + + ListFolderChildrenTool = ToolDescription{ + Name: "list-folder-children", + Description: "List children items of a specific folder", + InputSchema: schema{ + "type": "object", + "properties": props{ + "folder_id": prop("string", "The ID of the folder to list contents"), + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "top": prop("integer", "Number of items to return (max 999)"), + "orderby": prop("string", "Property to sort by (e.g., name, lastModifiedDateTime)"), + }, + "required": []string{"folder_id"}, + }, + } + + UploadFileTool = ToolDescription{ + Name: "upload-file", + Description: "Upload a small file to OneDrive (less than 4MB)", + InputSchema: schema{ + "type": "object", + "properties": props{ + "name": prop("string", "The name of the file to upload"), + "content": prop("string", "The content of the file as a string"), + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + "parent_id": prop("string", "The parent folder ID (optional, defaults to root)"), + "content_type": prop("string", "The content type of the file (e.g., text/plain)"), + }, + "required": []string{"name", "content"}, + }, + } + + GetItemTool = ToolDescription{ + Name: "get-item", + Description: "Get information about a specific item (file or folder) by ID", + InputSchema: schema{ + "type": "object", + "properties": props{ + "item_id": prop("string", "The ID of the item to get information about"), + "drive_id": prop("string", "The ID of the drive (omit to use default drive)"), + }, + "required": []string{"item_id"}, + }, + } + + GetDriveInfoTool = ToolDescription{ + Name: "get-drive-info", + Description: "Get information about a specific drive by ID or default drive", + InputSchema: schema{ + "type": "object", + "properties": props{ + "drive_id": prop("string", "The ID of the drive (omit to get info about default drive)"), + }, + }, + } + + OneDriveTools = []ToolDescription{ + ListDriveItemsTool, + ListRecentFilesTool, + ListSharedWithMeTool, + SearchDriveTool, + CreateFolderTool, + ListChildrenOfDriveTool, + ListFolderChildrenTool, + UploadFileTool, + GetItemTool, + GetDriveInfoTool, + } +) + +// Called when the tool is invoked +func Call(input CallToolRequest) (CallToolResult, error) { + token, ok := pdk.GetConfig("OAUTH_TOKEN") + if !ok { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("No OAUTH_TOKEN configured"), + }}, + }, nil + } + + args := input.Params.Arguments.(map[string]interface{}) + pdk.Log(pdk.LogDebug, fmt.Sprint("Args: ", args)) + + switch input.Params.Name { + case "list-drive-items": + return listDriveItems(token, args) + case "recent-files": + return getRecentFiles(token, args) + case "shared-with-me": + return getSharedWithMe(token, args) + case "search": + return searchDrive(token, args) + case "create-folder": + return createFolder(token, args) + case "list-drive-children": + return listDrives(token) + case "list-folder-children": + return listFolderChildren(token, args) + case "upload-file": + return uploadFile(token, args) + case "get-item": + return getItem(token, args) + case "get-drive-info": + return getDriveInfo(token, args) + default: + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Unknown tool " + input.Params.Name), + }}, + }, nil + } +} + +// Describe the tools provided by this servlet +func Describe() (ListToolsResult, error) { + return ListToolsResult{ + Tools: OneDriveTools, + }, nil +} + +// Helper function to create a pointer +func some[T any](t T) *T { + return &t +} + +// Schema related types for tool description +type SchemaProperty struct { + Type string `json:"type"` + Description string `json:"description,omitempty"` + Items *schema `json:"items,omitempty"` +} + +func prop(tpe, description string) SchemaProperty { + return SchemaProperty{Type: tpe, Description: description} +} + +type schema = map[string]interface{} +type props = map[string]SchemaProperty diff --git a/servlets/onedrive/onedrive.go b/servlets/onedrive/onedrive.go new file mode 100644 index 0000000..8c60388 --- /dev/null +++ b/servlets/onedrive/onedrive.go @@ -0,0 +1,518 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/extism/go-pdk" +) + +// Helper function to get the base URL for drive operations +func getDriveBaseURL(driveID string, path string) string { + if driveID == "" { + // Use default drive + return fmt.Sprintf("https://graph.microsoft.com/v1.0/me/drive/%s", path) + } else { + // Use specified drive + return fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/%s", driveID, path) + } +} + +// Helper function to add optional query parameters +func addPaginationParams(params *[]string, args map[string]interface{}) { + // Handle top parameter (with a maximum of 999) + if top, ok := args["top"].(float64); ok && top > 0 { + if top > 999 { + top = 999 // Maximum value + } + *params = append(*params, fmt.Sprintf("$top=%d", int(top))) + } + + // Handle orderby parameter + if orderby, ok := args["orderby"].(string); ok && orderby != "" { + *params = append(*params, fmt.Sprintf("$orderby=%s", orderby)) + } +} + +// List items in the root of the drive +func listDriveItems(token string, args map[string]interface{}) (CallToolResult, error) { + driveID, _ := args["drive_id"].(string) + baseURL := getDriveBaseURL(driveID, "root/children") + params := []string{} + + // Add pagination and sorting parameters + addPaginationParams(¶ms, args) + + // Build final URL + requestURL := baseURL + if len(params) > 0 { + requestURL = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to list drive items: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Get recently accessed files +func getRecentFiles(token string, args map[string]interface{}) (CallToolResult, error) { + driveID, _ := args["drive_id"].(string) + baseURL := getDriveBaseURL(driveID, "recent") + params := []string{} + + // Add pagination parameter + if top, ok := args["top"].(float64); ok && top > 0 { + if top > 999 { + top = 999 // Maximum value + } + params = append(params, fmt.Sprintf("$top=%d", int(top))) + } + + // Build final URL + requestURL := baseURL + if len(params) > 0 { + requestURL = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get recent files: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// List files shared with the user +func getSharedWithMe(token string, args map[string]interface{}) (CallToolResult, error) { + baseURL := "https://graph.microsoft.com/v1.0/me/drive/sharedWithMe" + params := []string{} + + // Add pagination parameter + if top, ok := args["top"].(float64); ok && top > 0 { + if top > 999 { + top = 999 // Maximum value + } + params = append(params, fmt.Sprintf("$top=%d", int(top))) + } + + // Build final URL + requestURL := baseURL + if len(params) > 0 { + requestURL = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get shared files: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Search OneDrive for files and folders +func searchDrive(token string, args map[string]interface{}) (CallToolResult, error) { + // Get search query + query, ok := args["query"].(string) + if !ok || query == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Search query is required"), + }}, + }, nil + } + + driveID, _ := args["drive_id"].(string) + var baseURL string + + if driveID == "" { + // Use proper search function format for default drive + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/me/drive/search(q='%s')", url.QueryEscape(query)) + } else { + // Use proper search function format for specified drive + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/search(q='%s')", driveID, url.QueryEscape(query)) + } + + params := []string{} + + // Add pagination parameter + if top, ok := args["top"].(float64); ok && top > 0 { + if top > 999 { + top = 999 // Maximum value + } + params = append(params, fmt.Sprintf("$top=%d", int(top))) + } + + // Build final URL + requestURL := baseURL + if len(params) > 0 { + requestURL = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to search drive: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Create a new folder in OneDrive +func createFolder(token string, args map[string]interface{}) (CallToolResult, error) { + // Get folder name + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Folder name is required"), + }}, + }, nil + } + + driveID, _ := args["drive_id"].(string) + + // Determine parent folder + var baseURL string + parent, hasParent := args["parent"].(string) + + if hasParent && parent != "" { + baseURL = getDriveBaseURL(driveID, fmt.Sprintf("items/%s/children", parent)) + } else { + baseURL = getDriveBaseURL(driveID, "root/children") + } + + // Create folder payload + folderPayload := map[string]interface{}{ + "name": name, + "folder": map[string]interface{}{}, + "@microsoft.graph.conflictBehavior": "rename", + } + + jsonData, err := json.Marshal(folderPayload) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal folder payload: %s", err)), + }}, + }, nil + } + + req := pdk.NewHTTPRequest(pdk.MethodPost, baseURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Content-Type", "application/json") + req.SetHeader("Accept", "application/json") + req.SetBody(jsonData) + + resp := req.Send() + if resp.Status() != 201 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create folder: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// List all drives +func listDrives(token string) (CallToolResult, error) { + requestURL := "https://graph.microsoft.com/v1.0/me/drives" + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to list drives: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// List children of a specific folder +func listFolderChildren(token string, args map[string]interface{}) (CallToolResult, error) { + // Get folder ID + folderId, ok := args["folder_id"].(string) + if !ok || folderId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Folder ID is required"), + }}, + }, nil + } + + driveID, _ := args["drive_id"].(string) + baseURL := getDriveBaseURL(driveID, fmt.Sprintf("items/%s/children", folderId)) + params := []string{} + + // Add pagination and sorting parameters + addPaginationParams(¶ms, args) + + // Build final URL + requestURL := baseURL + if len(params) > 0 { + requestURL = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, requestURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to list folder children: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Upload file (for files smaller than 4MB) +func uploadFile(token string, args map[string]interface{}) (CallToolResult, error) { + // Get file details + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("File name is required"), + }}, + }, nil + } + + content, ok := args["content"].(string) + if !ok { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("File content is required"), + }}, + }, nil + } + + contentType := "application/octet-stream" // Default content type + if ct, ok := args["content_type"].(string); ok && ct != "" { + contentType = ct + } + + driveID, _ := args["drive_id"].(string) + + // Determine parent folder + var baseURL string + parentId, hasParent := args["parent_id"].(string) + + if hasParent && parentId != "" { + if driveID == "" { + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/me/drive/items/%s:/%s:/content", parentId, url.PathEscape(name)) + } else { + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s:/content", driveID, parentId, url.PathEscape(name)) + } + } else { + if driveID == "" { + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/me/drive/root:/%s:/content", url.PathEscape(name)) + } else { + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/root:/%s:/content", driveID, url.PathEscape(name)) + } + } + + req := pdk.NewHTTPRequest(pdk.MethodPut, baseURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Content-Type", contentType) + req.SetBody([]byte(content)) + + resp := req.Send() + if resp.Status() != 201 && resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to upload file: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Get information about a specific item (file or folder) +func getItem(token string, args map[string]interface{}) (CallToolResult, error) { + // Get item ID + itemID, ok := args["item_id"].(string) + if !ok || itemID == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Item ID is required"), + }}, + }, nil + } + + driveID, _ := args["drive_id"].(string) + baseURL := getDriveBaseURL(driveID, fmt.Sprintf("items/%s", itemID)) + + req := pdk.NewHTTPRequest(pdk.MethodGet, baseURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get item: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + +// Get information about a specific drive or the default drive +func getDriveInfo(token string, args map[string]interface{}) (CallToolResult, error) { + var baseURL string + driveID, hasDriveID := args["drive_id"].(string) + + if hasDriveID && driveID != "" { + baseURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s", driveID) + } else { + baseURL = "https://graph.microsoft.com/v1.0/me/drive" + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, baseURL) + req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) + req.SetHeader("Accept", "application/json") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get drive info: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} diff --git a/servlets/onedrive/pdk.gen.go b/servlets/onedrive/pdk.gen.go new file mode 100644 index 0000000..6c04bdc --- /dev/null +++ b/servlets/onedrive/pdk.gen.go @@ -0,0 +1,218 @@ +// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT. +package main + +import ( + "errors" + + pdk "github.com/extism/go-pdk" +) + +//export call +func _Call() int32 { + var err error + _ = err + pdk.Log(pdk.LogDebug, "Call: getting JSON input") + var input CallToolRequest + err = pdk.InputJSON(&input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: calling implementation function") + output, err := Call(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: setting JSON output") + err = pdk.OutputJSON(output) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: returning") + return 0 +} + +//export describe +func _Describe() int32 { + var err error + _ = err + output, err := Describe() + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Describe: setting JSON output") + err = pdk.OutputJSON(output) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Describe: returning") + return 0 +} + +// +type BlobResourceContents struct { + // A base64-encoded string representing the binary data of the item. + Blob string `json:"blob"` + // The MIME type of this resource, if known. + MimeType *string `json:"mimeType,omitempty"` + // The URI of this resource. + Uri string `json:"uri"` +} + +// Used by the client to invoke a tool provided by the server. +type CallToolRequest struct { + Method *string `json:"method,omitempty"` + Params Params `json:"params"` +} + +// The server's response to a tool call. +// +// Any errors that originate from the tool SHOULD be reported inside the result +// object, with `isError` set to true, _not_ as an MCP protocol-level error +// response. Otherwise, the LLM would not be able to see that an error occurred +// and self-correct. +// +// However, any errors in _finding_ the tool, an error indicating that the +// server does not support tool calls, or any other exceptional conditions, +// should be reported as an MCP error response. +type CallToolResult struct { + Content []Content `json:"content"` + // Whether the tool call ended in an error. + // + // If not set, this is assumed to be false (the call was successful). + IsError *bool `json:"isError,omitempty"` +} + +// A content response. +// For text content set type to ContentType.Text and set the `text` property +// For image content set type to ContentType.Image and set the `data` and `mimeType` properties +type Content struct { + Annotations *TextAnnotation `json:"annotations,omitempty"` + // The base64-encoded image data. + Data *string `json:"data,omitempty"` + // The MIME type of the image. Different providers may support different image types. + MimeType *string `json:"mimeType,omitempty"` + // The text content of the message. + Text *string `json:"text,omitempty"` + Type ContentType `json:"type"` +} + +// +type ContentType string + +const ( + ContentTypeText ContentType = "text" + ContentTypeImage ContentType = "image" + ContentTypeResource ContentType = "resource" +) + +func (v ContentType) String() string { + switch v { + case ContentTypeText: + return `text` + case ContentTypeImage: + return `image` + case ContentTypeResource: + return `resource` + default: + return "" + } +} + +func stringToContentType(s string) (ContentType, error) { + switch s { + case `text`: + return ContentTypeText, nil + case `image`: + return ContentTypeImage, nil + case `resource`: + return ContentTypeResource, nil + default: + return ContentType(""), errors.New("unable to convert string to ContentType") + } +} + +// Provides one or more descriptions of the tools available in this servlet. +type ListToolsResult struct { + // The list of ToolDescription objects provided by this servlet. + Tools []ToolDescription `json:"tools"` +} + +// +type Params struct { + Arguments interface{} `json:"arguments,omitempty"` + Name string `json:"name"` +} + +// The sender or recipient of messages and data in a conversation. +type Role string + +const ( + RoleAssistant Role = "assistant" + RoleUser Role = "user" +) + +func (v Role) String() string { + switch v { + case RoleAssistant: + return `assistant` + case RoleUser: + return `user` + default: + return "" + } +} + +func stringToRole(s string) (Role, error) { + switch s { + case `assistant`: + return RoleAssistant, nil + case `user`: + return RoleUser, nil + default: + return Role(""), errors.New("unable to convert string to Role") + } +} + +// A text annotation +type TextAnnotation struct { + // Describes who the intended customer of this object or data is. + // + // It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + Audience []Role `json:"audience,omitempty"` + // Describes how important this data is for operating the server. + // + // A value of 1 means "most important," and indicates that the data is + // effectively required, while 0 means "least important," and indicates that + // the data is entirely optional. + Priority float32 `json:"priority,omitempty"` +} + +// +type TextResourceContents struct { + // The MIME type of this resource, if known. + MimeType *string `json:"mimeType,omitempty"` + // The text of the item. This must only be set if the item can actually be represented as text (not binary data). + Text string `json:"text"` + // The URI of this resource. + Uri string `json:"uri"` +} + +// Describes the capabilities and expected paramters of the tool function +type ToolDescription struct { + // A description of the tool + Description string `json:"description"` + // The JSON schema describing the argument input + InputSchema interface{} `json:"inputSchema"` + // The name of the tool. It should match the plugin / binding name. + Name string `json:"name"` +} diff --git a/servlets/onedrive/prepare.sh b/servlets/onedrive/prepare.sh new file mode 100644 index 0000000..2eeb17d --- /dev/null +++ b/servlets/onedrive/prepare.sh @@ -0,0 +1,90 @@ +#!/bin/bash +set -eou pipefail + +# Function to check if a command exists +command_exists () { + command -v "$1" >/dev/null 2>&1 +} + +# Function to compare version numbers for "less than" +version_lt() { + test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" = "$1" && test "$1" != "$2" +} + +missing_deps=0 + +# Check for Go +if ! (command_exists go); then + missing_deps=1 + echo "❌ Go (supported version between 1.20 - 1.24) is not installed." + echo "" + echo "To install Go, visit the official download page:" + echo "👉 https://go.dev/dl/" + echo "" + echo "Or install it using a package manager:" + echo "" + echo "🔹 macOS (Homebrew):" + echo " brew install go" + echo "" + echo "🔹 Ubuntu/Debian:" + echo " sudo apt-get -y install golang-go" + echo "" + echo "🔹 Arch Linux:" + echo " sudo pacman -S go" + echo "" + echo "🔹 Windows:" + echo " scoop install go" + echo "" +fi + +# Check for the right version of Go, needed by TinyGo (supports go 1.20 - 1.24) +if (command_exists go); then + compat=0 + for v in `seq 20 24`; do + if (go version | grep -q "go1.$v"); then + compat=1 + fi + done + + if [ $compat -eq 0 ]; then + echo "❌ Supported Go version is not installed. Must be Go 1.20 - 1.24." + echo "" + fi +fi + +ARCH=$(arch) + +# Check for TinyGo and its version +if ! (command_exists tinygo); then + missing_deps=1 + echo "❌ TinyGo is not installed." + echo "" + echo "To install TinyGo, visit the official download page:" + echo "👉 https://tinygo.org/getting-started/install/" + echo "" + echo "Or install it using a package manager:" + echo "" + echo "🔹 macOS (Homebrew):" + echo " brew tap tinygo-org/tools" + echo " brew install tinygo" + echo "" + echo "🔹 Ubuntu/Debian:" + echo " wget https://github.com/tinygo-org/tinygo/releases/download/v0.34.0/tinygo_0.34.0_$ARCH.deb" + echo " sudo dpkg -i tinygo_0.34.0_$ARCH.deb" + echo "" + echo "🔹 Arch Linux:" + echo " pacman -S extra/tinygo" + echo "" + echo "🔹 Windows:" + echo " scoop install tinygo" + echo "" +else + # Check TinyGo version + tinygo_version=$(tinygo version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n1) + if version_lt "$tinygo_version" "0.34.0"; then + missing_deps=1 + echo "❌ TinyGo version must be >= 0.34.0 (current version: $tinygo_version)" + echo "Please update TinyGo to a newer version." + echo "" + fi +fi \ No newline at end of file diff --git a/servlets/onedrive/xtp.toml b/servlets/onedrive/xtp.toml new file mode 100644 index 0000000..849a517 --- /dev/null +++ b/servlets/onedrive/xtp.toml @@ -0,0 +1,17 @@ +app_id = "app_01je4dgpcyfvgrz8f1ys3pbxas" + +# This is where 'xtp plugin push' expects to find the wasm file after the build script has run. +bin = "dist/plugin.wasm" +extension_point_id = "ext_01je4jj1tteaktf0zd0anm8854" +# This is the 'binding' name used for the plugin. +name = "onedrive" + +[scripts] +# xtp plugin build runs this script to generate the wasm file +build = "mkdir -p dist && tinygo build -buildmode c-shared -target wasip1 -o dist/plugin.wasm ." + +# xtp plugin init runs this script to format the code +format = "go fmt && go mod tidy && goimports -w main.go" + +# xtp plugin init runs this script before running the format script +prepare = "bash prepare.sh && go get ./..."