tools

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BulletTool = mcp.GenericTool[BulletInput]{
	Name: "manage_bullet",
	Description: "Add, remove or complete bullet points.\n" +
		"The method parameter defines the action to take: 'add', 'remove', 'complete', 'toggle' or 'set_content'.\n" +
		"- 'add': Adds a new bullet point at the specified index under the given header_uid. Requires 'content' and 'checkbox' parameters.\n" +
		"- 'remove': Removes the bullet point identified by its uid.\n" +
		"- 'complete': Marks the bullet point as completed (Checked).\n" +
		"- 'toggle': Toggles the checkbox status of the bullet point between Checked and Unchecked.\n" +
		"- 'set_content': Updates the content of the bullet point. Requires 'content' parameter.\n\n" +
		"The 'header_uid' parameter specifies the parent header under which the bullet point resides.\n" +
		"The 'bullet_index' parameter specifies the position of the bullet point under the header (0-based index).\n\n" +
		"When targeting a bullet, the uid is constructed as `header_uid + '.b' + bullet_index`.\n" +
		"Bullets are hierarchical meaning that bullets can have sub-bullets. Sub-bullets will use parent_bullet_uid + '.b' + bullet_sub_index like header_uid.b0.b1\n" +
		"The add method is special as it requires the header_uid to be passed directly without any bullet index. The index will be determined by the tool itself.",
	Callback: bulletFunc,
}
View Source
var HeaderTool = mcp.GenericTool[HeaderInput]{
	Name: "manage_header",
	Description: "Add; remove or update headers in an Org file.\n" +
		"The method parameter defines the action to take: 'add'; 'remove'; 'update'.\n" +
		"For any method you can use a depth parameter to specify how many levels of children to return.\n" +
		"- 'add': Adds a new header at the specified index under the given parent_uid (pass this in the uid field of the function). Requires 'content' parameter.\n" +
		"- 'remove': Removes the header identified by its uid.\n" +
		"- 'update': Updates the header's content; status; or tags. Requires 'content'; 'status'; or 'tags' parameters.\n\n" +
		"It is recommended to pass uid's as string to the function. While they will almost certainly be numbers; this is not guaranteed.",
	Callback: func(ctx context.Context, input HeaderInput, options mcp.FuncOptions) (resp []any, err error) {
		var path string

		if input.Path == "" {
			path = options.DefaultPath
		} else {
			path = input.Path
		}

		orgFile, err := mcp.LoadOrgFile(ctx, path)
		if err != nil {
			return
		}

		if len(input.Columns) == 0 {
			uidCol := orgmcp.ColUid
			contentCol := orgmcp.ColContent
			input.Columns = []*orgmcp.Column{&uidCol, &contentCol}
		}

		var ordered []orgmcp.Render
		results := map[orgmcp.Uid]orgmcp.Render{}

		for _, headerOp := range input.Headers {
			switch headerOp.Method {
			case "add":
				parent, ok := orgFile.GetUid(orgmcp.NewUid(headerOp.Uid)).Split()
				if !ok {
					err = errors.New("invalid parent UID for adding header")
					return
				}

				var tags option.Option[orgmcp.TagList]
				if len(headerOp.Tags) == 0 {
					tags = option.None[orgmcp.TagList]()
				} else {
					tags = option.Some(orgmcp.TagList(headerOp.Tags))
				}

				newHeader := orgmcp.NewHeader(
					orgmcp.HeaderStatus(headerOp.Status),
					headerOp.Content,
				)

				newHeader.Tags = tags
				newHeader.SetLevel(parent.Level() + 1)

				parent.AddChildren(&newHeader)

				depth := 1
				if headerOp.Depth != nil {
					depth = *headerOp.Depth
				}

				results[newHeader.Uid()] = &newHeader

				for _, child := range newHeader.ChildrenRec(depth) {
					results[child.Uid()] = child
				}
			case "remove":
				header, ok := orgFile.GetUid(orgmcp.NewUid(headerOp.Uid)).Split()
				if !ok {
					err = errors.New("invalid header UID for removal")
					return
				}

				parent, ok := orgFile.GetUid(header.ParentUid()).Split()
				if !ok {
					err = errors.New("Missing or invalid parent for header removal")
					return
				}

				err = parent.RemoveChildren(orgmcp.NewUid(headerOp.Uid))

				if err != nil {
					return
				}

				depth := 1
				if headerOp.Depth != nil {
					depth = *headerOp.Depth
				}

				results[header.Uid()] = header

				for _, child := range header.ChildrenRec(depth) {
					results[child.Uid()] = child
				}
			case "update":
				header, ok := option.Cast[orgmcp.Render, *orgmcp.Header](orgFile.GetUid(orgmcp.NewUid(headerOp.Uid))).Split()
				if !ok {
					err = errors.New("invalid header UID for update or not a header")
					return
				}

				if headerOp.Content != "" {
					header.Content = headerOp.Content
				}

				if headerOp.Status != "" {
					header.SetStatus(orgmcp.StatusFromString(headerOp.Status))
				}

				if len(headerOp.Tags) != 0 {
					tags := option.Some(orgmcp.TagList(headerOp.Tags))
					header.Tags = tags
				}

				depth := 1
				if headerOp.Depth != nil {
					depth = *headerOp.Depth
				}

				results[header.Uid()] = header

				for _, child := range header.ChildrenRec(depth) {
					results[child.Uid()] = child
				}
			default:
				err = errors.New("invalid method for header management")
			}

			if err != nil {
				return
			}
		}

		locationTable := orgFile.BuildLocationTable()
		ordered = append(ordered, slices.Collect(maps.Values(results))...)
		slices.SortFunc(ordered, func(a, b orgmcp.Render) int {
			return (*locationTable)[a.Uid()] - (*locationTable)[b.Uid()]
		})
		resp = append(resp, orgmcp.PrintCsv(ordered, input.Columns))

		diff, err := mcp.WriteOrgFileToDisk(ctx, orgFile, path)

		if input.ShowDiff {
			resp = append(resp, diff)
		}

		return
	},
}
View Source
var StatusTool = mcp.GenericTool[StatusInputSchema]{
	Name: "status_overview",
	Description: `
Provides an overview of task statuses in the Org file.
It counts the number of tasks in each status category (TODO, NEXT, PROG, REVW, DONE, DELG).
You will also receive all the uid's of the headers for each category in a list.

This tool will also return an overview of all tags used in the Org file and the count of how many times each tag is used.
Remember however that tags are recursive children will inherit the tags of their parents,
so a header with the tag "project" and a child header without any tags will still be counted as having the "project" tag.
`,

	Callback: func(ctx context.Context, input StatusInputSchema, options mcp.FuncOptions) (resp []any, err error) {
		var path string
		if input.Path == "" {
			path = options.DefaultPath
		} else {
			path = input.Path
		}

		orgFile, err := mcp.LoadOrgFile(ctx, path)
		if err != nil {
			return
		}

		resp = []any{map[string]any{
			"status_overview": orgFile.GetStatusOverview(),
			"tag_overview":    orgFile.GetTagOverview(),
		}}

		_, err = mcp.WriteOrgFileToDisk(ctx, orgFile, path)

		return
	},
}
View Source
var TextTool = mcp.GenericTool[TextInputSchema]{
	Name: "manage_text",
	Description: `
Add, update or remove text content in an Org file.
Plain text is a special type of content that cannot contain any nested elements.
It is solely used for storing text content within a header or bullet point.

Most of the time it will be more correct to use a bullet point to store text content as it allows for better organization and structuring of the content.
But there are situations where plain text is more appropriate, such as large block of text without structure, like github issues etc.

## Methods
` +
		"`add`: Adds new text content to the specified parent element. The parent is passed via the uid parameter.\n" +
		"`update`: Updates the text content of the specified element. The element is identified by its uid.\n" +
		"`remove`: Removes the text content of the specified element. The element is identified by its uid.\n" +
		`
## UID Constructions
You can target either a header or a bullet point when adding text content. The uid will be the parent itself.
Otherwise the uid construction is similar to the bullet tool.
The text index is relative to the parent element, you can have multiple text elements under the same parent.
The index is based on newline separation, but is deligneated by the parent element.

## Diff
You always have the options with any modification to show a diff of the changes made.
This can inform both you as well as the user about what exactly a tool call changed, and always you to undo changes if needed.
` +
		"`parent_uid + .t + text_index`\n",
	Callback: func(ctx context.Context, input TextInputSchema, options mcp.FuncOptions) (resp []any, err error) {
		var path string
		if input.Path == "" {
			path = options.DefaultPath
		} else {
			path = input.Path
		}

		orgFile, err := mcp.LoadOrgFile(ctx, path)
		if err != nil {
			return
		}

		affectedCount := 0
		affectedItems := map[orgmcp.Uid]orgmcp.Render{}

		for _, mt := range input.Texts {
			var selected orgmcp.Render
			var ok bool
			if selected, ok = orgFile.GetUid(orgmcp.NewUid(mt.Uid)).Split(); !ok {
				resp = append(resp, fmt.Sprintf("Uid %s not found in %s", mt.Uid, path))
				continue
			}

			switch mt.Method {
			case "add":
				if strings.Contains(mt.Content, "\n") {
					resp = append(resp, "Content for update method should not contain newlines. You should update each text element separately. As a fallback the newlines will be replaced with spaces.")
					mt.Content = strings.ReplaceAll(mt.Content, "\n", " ")
				}

				newPlainText := orgmcp.NewPlainText(mt.Content)
				selected.AddChildren(&newPlainText)
			case "update":
				if strings.Contains(mt.Content, "\n") {
					resp = append(resp, "Content for update method should not contain newlines. You should update each text element separately. As a fallback the newlines will be replaced with spaces.")
					mt.Content = strings.ReplaceAll(mt.Content, "\n", " ")
				}

				if plain, ok := selected.(*orgmcp.PlainText); ok {
					plain.SetContent(mt.Content)
					affectedItems[plain.Uid()] = plain
					affectedCount += 1
				} else {
					resp = append(resp, fmt.Sprintf("Uid %s is not a plain text element, cannot update content", mt.Uid))
				}

				continue
			case "remove":
				p_uid := selected.ParentUid()
				if parent, ok := orgFile.GetUid(p_uid).Split(); ok {
					parent.RemoveChildren(selected.Uid())
				} else {
					resp = append(resp, fmt.Sprintf("Could not find parent for uid %s, skipping removal", mt.Uid))
				}
			}

			affectedItems[selected.Uid()] = selected
			for _, child := range selected.ChildrenRec(-1) {
				affectedItems[child.Uid()] = child
			}

			affectedCount += 1
		}

		ordered := []orgmcp.Render{}

		if input.ShowAffected == nil || *input.ShowAffected == true {
			locationTable := orgFile.BuildLocationTable()
			ordered = append(ordered, itertools.Collect(maps.Values(affectedItems))...)

			slices.SortFunc(ordered, func(a, b orgmcp.Render) int {
				return (*locationTable)[a.Uid()] - (*locationTable)[b.Uid()]
			})

			resp = append(resp, orgmcp.PrintCsv(ordered, input.Columns))
			resp = append(resp, map[string]any{
				"affected_count": affectedCount,
			})
		}

		diff, err := mcp.WriteOrgFileToDisk(ctx, orgFile, path)
		if input.ShowDiff {
			resp = append(resp, diff)
		}

		return
	},
}
View Source
var VectorSearch = mcp.GenericTool[VectorSearchInput]{
	Name: "vector_search",
	Description: "Perform a vector search on all headers in the org file based on the provided query string. " +
		"Returns the top N most relevant headers.\n" +
		"It is optimal to include as much information as possible in the query, overflowing the text limit is hard. " +
		"So include context like timeframes, people involved, locations, etc. to get the best results.",
	Callback: func(ctx context.Context, input VectorSearchInput, options mcp.FuncOptions) (resp []any, err error) {
		filePath := options.DefaultPath
		if input.Path != "" {
			filePath = input.Path
		}

		if input.TopN <= 0 {
			input.TopN = 3.0
		}

		of, err := mcp.LoadOrgFile(ctx, filePath)
		if err != nil {
			return
		}

		headers, err := of.VectorSearch(input.Query, input.TopN)

		resp = append(resp, slice.Map(headers, func(r orgmcp.Render) map[string]any {
			builder := strings.Builder{}
			r.Render(&builder, 1)

			return map[string]any{
				"uid":        r.Uid().String(),
				"content":    builder.String(),
				"parent_uid": r.ParentUid().String(),
			}
		}))

		return
	},
}
View Source
var ViewTool = mcp.GenericTool[ViewInput]{
	Name: "query_items",
	Description: `
# query_items
  View and filter Org items.

## Arguments
  - items: Array of filters (OR logic between items).
    - uid: string (optional)
    - status: "TODO" | "DONE" | "CHECKED" | ...
    - content: string (regex match)
    - tags: Array<string> (all tags must be present)
    - date_filter:
      - match: "SCHEDULED" | "DEADLINE" | "CLOSED"
      - range: number (days, negative for past)
      - show_closed: boolean
    - depth: number (optional, defaults to 1), determines how many levels of children to include in the CSV.
  - path: string (defaults to ./.tasks.org), unless you encounter errors about file not found or otherwise specified leave this empty.
  - columns: Array of column names to include in the output CSV. Defaults to [UID ; PREVIEW]. See the columns section below for available columns.

## Summary
  Returns a CSV of matching items. See the columns section in the common instructions for what columns you can specify.
`,
	Callback: func(ctx context.Context, input ViewInput, options mcp.FuncOptions) (resp []any, err error) {
		var path string
		if input.Path == "" {
			path = options.DefaultPath
		} else {
			path = input.Path
		}

		orgFile, err := mcp.LoadOrgFile(ctx, path)
		if err != nil {
			return
		}

		if len(input.Columns) == 0 {
			input.Columns = []*orgmcp.Column{&orgmcp.ColUidValue, &orgmcp.ColPreviewValue}
		}

		results := map[orgmcp.Uid]orgmcp.Render{}

		for _, item := range input.Items {
			depth := 1
			if item.Depth != nil {
				depth = *item.Depth
			}

			for _, render := range orgFile.ChildrenRec(-1) {
				if item.Uid != "" && render.Uid().String() != item.Uid {
					continue
				}

				if item.Status != nil && render.Status() != *item.Status {
					continue
				}

				if item.Content != "" {
					reg, err := regexp.Compile(item.Content)
					if err != nil {
						return nil, err
					}

					preview := render.Preview(-1)
					if !reg.MatchString(preview) {
						continue
					}
				}

				if item.Date != nil {
					match, err := FilterDate(render, item.Date)
					if err != nil {
						return nil, err
					}

					if !match {
						continue
					}
				}

				if len(item.Tags) > 0 {
					foundAll := true
					for _, tag := range item.Tags {
						if !slices.Contains(render.TagList(), tag) {
							foundAll = false
							break
						}
					}

					if !foundAll {
						continue
					}
				}

				results[render.Uid()] = render
				for _, child := range render.ChildrenRec(depth) {
					results[child.Uid()] = child
				}
			}
		}

		locationTable := orgFile.GetLocationTable()

		ordered := slices.Collect(maps.Values(results))
		slices.SortFunc(ordered, func(a, b orgmcp.Render) int {
			return (*locationTable)[a.Uid()] - (*locationTable)[b.Uid()]
		})

		resp = append(resp, orgmcp.PrintCsv(ordered, input.Columns))
		_, err = mcp.WriteOrgFileToDisk(ctx, orgFile, path)

		return
	},
}

Functions

func FilterDate

func FilterDate(r orgmcp.Render, dateFilter *DateFilter) (match bool, err error)

func GetDiffOnly

func GetDiffOnly(of orgmcp.OrgFile, filePath string) (res string, err error)

GetDiffOnly renders the OrgFile and returns a diff against the current disk content without modifying the file on disk.

Types

type BulletInput

type BulletInput struct {
	Bullets      []BulletValue    `json:"bullets,omitempty" jsonschema:"description=List of bullet point operations to perform."`
	Path         string           `json:"path,omitempty" jsonschema:"description=Optional file path; defaults to ./.tasks.org."`
	ShowDiff     bool             `json:"show_diff,omitempty" jsonschema:"description=Whether to show the diff of changes made to the Org file."`
	ShowAffected *bool            `` /* 197-byte string literal not displayed */
	Columns      []*orgmcp.Column `` /* 137-byte string literal not displayed */
}

type BulletValue

type BulletValue struct {
	Uid      string `` /* 152-byte string literal not displayed */
	Method   string `` /* 139-byte string literal not displayed */
	Content  string `json:"content,omitempty" jsonschema:"description=Text content of the bullet."`
	Checkbox string `json:"checkbox,omitempty" jsonschema:"description=Checkbox status for the new bullet.,enum=None;Unchecked;Checked"`
}

type ColumnList

type ColumnList []*orgmcp.Column

func (ColumnList) GetSchema

func (cl ColumnList) GetSchema() map[string]any

type DateFilter

type DateFilter struct {
	Match      string  `` /* 127-byte string literal not displayed */
	ShowClosed bool    `` /* 186-byte string literal not displayed */
	Date       *string `json:"date,omitempty" jsonschema:"description=The date to match against in YYYY-MM-DD format. Will default to today."`
	Range      *int    `` /* 468-byte string literal not displayed */
}

type HeaderInput

type HeaderInput struct {
	Headers  []HeaderValue    `` /* 132-byte string literal not displayed */
	Path     string           `` /* 224-byte string literal not displayed */
	ShowDiff bool             `` /* 170-byte string literal not displayed */
	Columns  []*orgmcp.Column `` /* 137-byte string literal not displayed */
}

type HeaderValue

type HeaderValue struct {
	Uid     string   `json:"uid" jsonschema:"description=UID of the header to modify or the parent_uid when adding."`
	Method  string   `json:"method" jsonschema:"description=The method by which to manage the header.;enum=add;remove;update"`
	Status  string   `` /* 259-byte string literal not displayed */
	Content string   `` /* 129-byte string literal not displayed */
	Tags    []string `` /* 208-byte string literal not displayed */
	Depth   *int     `` /* 172-byte string literal not displayed */
}

type StatusInputSchema

type StatusInputSchema struct {
	Path string `` /* 224-byte string literal not displayed */
}

type TextInputSchema

type TextInputSchema struct {
	Texts        []TextInputValue `json:"texts" jsonschema:"description=The list of text modifications to perform"`
	Path         string           `` /* 159-byte string literal not displayed */
	ShowDiff     bool             `` /* 127-byte string literal not displayed */
	ShowAffected *bool            `` /* 212-byte string literal not displayed */
	Columns      []*orgmcp.Column `` /* 137-byte string literal not displayed */
}

type TextInputValue

type TextInputValue struct {
	Uid     string `` /* 218-byte string literal not displayed */
	Method  string `` /* 175-byte string literal not displayed */
	Content string `` /* 138-byte string literal not displayed */
}

type VectorSearchInput

type VectorSearchInput struct {
	Query string `json:"query" jsonschema:"description=The search query string.,required=true"`
	TopN  int    `json:"top_n,omitempty" jsonschema:"description=The number of top relevant headers to return."`
	Path  string `json:"path,omitempty" jsonschema:"description=An optional file path; will default to the configured org file. (./.tasks.org)"`
}

type ViewInput

type ViewInput struct {
	Items   []ViewItem `json:"items" jsonschema:"description=List of items to view based on their UIDs and filters.,required=true"`
	Columns ColumnList `json:"columns,omitempty"`
	Path    string     `json:"path,omitempty" jsonschema:"description=An optional file path; will default to ./.tasks.org"`
}

type ViewItem

type ViewItem struct {
	Uid     string               `json:"uid,omitempty" jsonschema:"description=UID of the header to view. If not provided, all headers are considered."`
	Status  *orgmcp.RenderStatus `` /* 183-byte string literal not displayed */
	Content string               `` /* 212-byte string literal not displayed */
	Tags    []string             `` /* 131-byte string literal not displayed */
	Depth   *int                 `json:"depth,omitempty" jsonschema:"description=Depth of child headers to include. Default is 1 (only direct children)."`
	Date    *DateFilter          `` /* 127-byte string literal not displayed */
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL