chore(mcp): improve tool discoverability, add orientation tools and evals
Make the OpenAPI-driven MCP surface more usable by agents, following the mcp-builder guidance. - Enrich proto descriptions (single source of truth, flows to OpenAPI + MCP tool descriptions): document the memo `filter` CEL grammar with fields and examples (replacing the dangling "Refer to Shortcut.filter"), clarify the created_ts/updated_ts vs create_time/update_time naming, the visibility enum, the declarative replace semantics of Set* ops, and steer tag filters to `"x" in tags` (not the unsupported `tag == "x"`). - Mark SetMemoAttachments / SetMemoRelations idempotent via a per-operation override the HTTP-method heuristic can't express. - Curate two read-only orientation tools: shortcut_list_shortcuts (surfaces reusable CEL filters) and auth_get_current_user (the single allowed auth/identity op, for resolving the current user); guard test updated to keep the rest of the auth/user surface excluded. - Add a task-level evaluation suite (server/router/mcp/evals) with 10 verified questions, pinned to the deterministic demo seed.
This commit is contained in:
parent
daa71d0456
commit
047175dbed
16 changed files with 428 additions and 65 deletions
|
|
@ -15,7 +15,9 @@ import "google/protobuf/timestamp.proto";
|
|||
option go_package = "gen/api/v1";
|
||||
|
||||
service MemoService {
|
||||
// CreateMemo creates a memo.
|
||||
// CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
// (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
// The memo is owned by the authenticated user; requires authentication.
|
||||
rpc CreateMemo(CreateMemoRequest) returns (Memo) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/memos"
|
||||
|
|
@ -46,7 +48,9 @@ service MemoService {
|
|||
option (google.api.http) = {delete: "/api/v1/{name=memos/*}"};
|
||||
option (google.api.method_signature) = "name";
|
||||
}
|
||||
// SetMemoAttachments sets attachments for a memo.
|
||||
// SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all attachments. Idempotent.
|
||||
rpc SetMemoAttachments(SetMemoAttachmentsRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = {
|
||||
patch: "/api/v1/{name=memos/*}/attachments"
|
||||
|
|
@ -59,7 +63,9 @@ service MemoService {
|
|||
option (google.api.http) = {get: "/api/v1/{name=memos/*}/attachments"};
|
||||
option (google.api.method_signature) = "name";
|
||||
}
|
||||
// SetMemoRelations sets relations for a memo.
|
||||
// SetMemoRelations replaces the full set of relations on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all relations. Idempotent.
|
||||
rpc SetMemoRelations(SetMemoRelationsRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = {
|
||||
patch: "/api/v1/{name=memos/*}/relations"
|
||||
|
|
@ -90,7 +96,8 @@ service MemoService {
|
|||
option (google.api.http) = {get: "/api/v1/{name=memos/*}/reactions"};
|
||||
option (google.api.method_signature) = "name";
|
||||
}
|
||||
// UpsertMemoReaction upserts a reaction for a memo.
|
||||
// UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
// memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
rpc UpsertMemoReaction(UpsertMemoReactionRequest) returns (Reaction) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/{name=memos/*}/reactions"
|
||||
|
|
@ -139,10 +146,14 @@ service MemoService {
|
|||
}
|
||||
}
|
||||
|
||||
// Visibility controls who can read a memo.
|
||||
enum Visibility {
|
||||
VISIBILITY_UNSPECIFIED = 0;
|
||||
// PRIVATE: only the creator can read the memo.
|
||||
PRIVATE = 1;
|
||||
// PROTECTED: signed-in users of the instance can read the memo.
|
||||
PROTECTED = 2;
|
||||
// PUBLIC: anyone, including anonymous visitors, can read the memo.
|
||||
PUBLIC = 3;
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +233,8 @@ message Memo {
|
|||
string content = 7 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// The visibility of the memo.
|
||||
// One of PRIVATE (creator only), PROTECTED (signed-in users), or
|
||||
// PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified.
|
||||
Visibility visibility = 9 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// Output only. The tags extracted from the content.
|
||||
|
|
@ -305,12 +318,24 @@ message ListMemosRequest {
|
|||
// Default to "create_time desc".
|
||||
// Supports comma-separated list of fields following AIP-132.
|
||||
// Example: "pinned desc, create_time desc" or "update_time asc"
|
||||
// Supported fields: pinned, create_time, update_time, name
|
||||
// Supported fields: pinned, create_time, update_time, name.
|
||||
// Note: order_by uses create_time / update_time, while the filter
|
||||
// expression uses created_ts / updated_ts for the same timestamps.
|
||||
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
|
||||
|
||||
// Optional. Filter to apply to the list results.
|
||||
// Filter is a CEL expression to filter memos.
|
||||
// Refer to `Shortcut.filter`.
|
||||
// Optional. A CEL expression to filter memos. Combine terms with && and ||.
|
||||
// Available fields:
|
||||
// content (string), creator (string, e.g. "users/1"),
|
||||
// created_ts / updated_ts (timestamp), pinned (bool),
|
||||
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||
// has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
||||
// Note: the time fields here are created_ts / updated_ts, which differ from
|
||||
// the create_time / update_time names used by order_by.
|
||||
// Examples:
|
||||
// pinned == true && visibility == "PUBLIC"
|
||||
// tags.exists(t, t == "urgent")
|
||||
// content.contains("roadmap") && created_ts > now - duration("168h")
|
||||
string filter = 5 [(google.api.field_behavior) = OPTIONAL];
|
||||
|
||||
// Optional. If true, show deleted memos in the response.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import "google/protobuf/field_mask.proto";
|
|||
option go_package = "gen/api/v1";
|
||||
|
||||
service ShortcutService {
|
||||
// ListShortcuts returns a list of shortcuts for a user.
|
||||
// ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
// reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
// to the ListMemos `filter` argument to reuse a saved view.
|
||||
rpc ListShortcuts(ListShortcutsRequest) returns (ListShortcutsResponse) {
|
||||
option (google.api.http) = {get: "/api/v1/{parent=users/*}/shortcuts"};
|
||||
option (google.api.method_signature) = "parent";
|
||||
|
|
@ -64,7 +66,8 @@ message Shortcut {
|
|||
// The title of the shortcut.
|
||||
string title = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
|
||||
// The filter expression for the shortcut.
|
||||
// The CEL filter expression for the shortcut, using the same grammar as the
|
||||
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||
string filter = 3 [(google.api.field_behavior) = OPTIONAL];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,9 @@ const (
|
|||
|
||||
// MemoServiceClient is a client for the memos.api.v1.MemoService service.
|
||||
type MemoServiceClient interface {
|
||||
// CreateMemo creates a memo.
|
||||
// CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
// (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
// The memo is owned by the authenticated user; requires authentication.
|
||||
CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error)
|
||||
// ListMemos lists memos with pagination and filter.
|
||||
ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error)
|
||||
|
|
@ -103,11 +105,15 @@ type MemoServiceClient interface {
|
|||
UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error)
|
||||
// DeleteMemo deletes a memo.
|
||||
DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// SetMemoAttachments sets attachments for a memo.
|
||||
// SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all attachments. Idempotent.
|
||||
SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// ListMemoAttachments lists attachments for a memo.
|
||||
ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error)
|
||||
// SetMemoRelations sets relations for a memo.
|
||||
// SetMemoRelations replaces the full set of relations on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all relations. Idempotent.
|
||||
SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// ListMemoRelations lists relations for a memo.
|
||||
ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error)
|
||||
|
|
@ -117,7 +123,8 @@ type MemoServiceClient interface {
|
|||
ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error)
|
||||
// ListMemoReactions lists reactions for a memo.
|
||||
ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error)
|
||||
// UpsertMemoReaction upserts a reaction for a memo.
|
||||
// UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
// memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
|
|
@ -396,7 +403,9 @@ func (c *memoServiceClient) BatchGetLinkMetadata(ctx context.Context, req *conne
|
|||
|
||||
// MemoServiceHandler is an implementation of the memos.api.v1.MemoService service.
|
||||
type MemoServiceHandler interface {
|
||||
// CreateMemo creates a memo.
|
||||
// CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
// (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
// The memo is owned by the authenticated user; requires authentication.
|
||||
CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error)
|
||||
// ListMemos lists memos with pagination and filter.
|
||||
ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error)
|
||||
|
|
@ -406,11 +415,15 @@ type MemoServiceHandler interface {
|
|||
UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error)
|
||||
// DeleteMemo deletes a memo.
|
||||
DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// SetMemoAttachments sets attachments for a memo.
|
||||
// SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all attachments. Idempotent.
|
||||
SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// ListMemoAttachments lists attachments for a memo.
|
||||
ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error)
|
||||
// SetMemoRelations sets relations for a memo.
|
||||
// SetMemoRelations replaces the full set of relations on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all relations. Idempotent.
|
||||
SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// ListMemoRelations lists relations for a memo.
|
||||
ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error)
|
||||
|
|
@ -420,7 +433,8 @@ type MemoServiceHandler interface {
|
|||
ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error)
|
||||
// ListMemoReactions lists reactions for a memo.
|
||||
ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error)
|
||||
// UpsertMemoReaction upserts a reaction for a memo.
|
||||
// UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
// memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ const (
|
|||
|
||||
// ShortcutServiceClient is a client for the memos.api.v1.ShortcutService service.
|
||||
type ShortcutServiceClient interface {
|
||||
// ListShortcuts returns a list of shortcuts for a user.
|
||||
// ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
// reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
// to the ListMemos `filter` argument to reuse a saved view.
|
||||
ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error)
|
||||
// GetShortcut gets a shortcut by name.
|
||||
GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error)
|
||||
|
|
@ -145,7 +147,9 @@ func (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, req *connect
|
|||
|
||||
// ShortcutServiceHandler is an implementation of the memos.api.v1.ShortcutService service.
|
||||
type ShortcutServiceHandler interface {
|
||||
// ListShortcuts returns a list of shortcuts for a user.
|
||||
// ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
// reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
// to the ListMemos `filter` argument to reuse a saved view.
|
||||
ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error)
|
||||
// GetShortcut gets a shortcut by name.
|
||||
GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,17 @@ const (
|
|||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Visibility controls who can read a memo.
|
||||
type Visibility int32
|
||||
|
||||
const (
|
||||
Visibility_VISIBILITY_UNSPECIFIED Visibility = 0
|
||||
Visibility_PRIVATE Visibility = 1
|
||||
Visibility_PROTECTED Visibility = 2
|
||||
Visibility_PUBLIC Visibility = 3
|
||||
// PRIVATE: only the creator can read the memo.
|
||||
Visibility_PRIVATE Visibility = 1
|
||||
// PROTECTED: signed-in users of the instance can read the memo.
|
||||
Visibility_PROTECTED Visibility = 2
|
||||
// PUBLIC: anyone, including anonymous visitors, can read the memo.
|
||||
Visibility_PUBLIC Visibility = 3
|
||||
)
|
||||
|
||||
// Enum value maps for Visibility.
|
||||
|
|
@ -231,6 +235,8 @@ type Memo struct {
|
|||
// Required. The content of the memo in Markdown format.
|
||||
Content string `protobuf:"bytes,7,opt,name=content,proto3" json:"content,omitempty"`
|
||||
// The visibility of the memo.
|
||||
// One of PRIVATE (creator only), PROTECTED (signed-in users), or
|
||||
// PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified.
|
||||
Visibility Visibility `protobuf:"varint,9,opt,name=visibility,proto3,enum=memos.api.v1.Visibility" json:"visibility,omitempty"`
|
||||
// Output only. The tags extracted from the content.
|
||||
Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||
|
|
@ -532,11 +538,26 @@ type ListMemosRequest struct {
|
|||
// Default to "create_time desc".
|
||||
// Supports comma-separated list of fields following AIP-132.
|
||||
// Example: "pinned desc, create_time desc" or "update_time asc"
|
||||
// Supported fields: pinned, create_time, update_time, name
|
||||
// Supported fields: pinned, create_time, update_time, name.
|
||||
// Note: order_by uses create_time / update_time, while the filter
|
||||
// expression uses created_ts / updated_ts for the same timestamps.
|
||||
OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"`
|
||||
// Optional. Filter to apply to the list results.
|
||||
// Filter is a CEL expression to filter memos.
|
||||
// Refer to `Shortcut.filter`.
|
||||
// Optional. A CEL expression to filter memos. Combine terms with && and ||.
|
||||
// Available fields:
|
||||
//
|
||||
// content (string), creator (string, e.g. "users/1"),
|
||||
// created_ts / updated_ts (timestamp), pinned (bool),
|
||||
// visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||
// tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||
// has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
||||
//
|
||||
// Note: the time fields here are created_ts / updated_ts, which differ from
|
||||
// the create_time / update_time names used by order_by.
|
||||
// Examples:
|
||||
//
|
||||
// pinned == true && visibility == "PUBLIC"
|
||||
// tags.exists(t, t == "urgent")
|
||||
// content.contains("roadmap") && created_ts > now - duration("168h")
|
||||
Filter string `protobuf:"bytes,5,opt,name=filter,proto3" json:"filter,omitempty"`
|
||||
// Optional. If true, show deleted memos in the response.
|
||||
ShowDeleted bool `protobuf:"varint,6,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"`
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ const (
|
|||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type MemoServiceClient interface {
|
||||
// CreateMemo creates a memo.
|
||||
// CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
// (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
// The memo is owned by the authenticated user; requires authentication.
|
||||
CreateMemo(ctx context.Context, in *CreateMemoRequest, opts ...grpc.CallOption) (*Memo, error)
|
||||
// ListMemos lists memos with pagination and filter.
|
||||
ListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error)
|
||||
|
|
@ -56,11 +58,15 @@ type MemoServiceClient interface {
|
|||
UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*Memo, error)
|
||||
// DeleteMemo deletes a memo.
|
||||
DeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// SetMemoAttachments sets attachments for a memo.
|
||||
// SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all attachments. Idempotent.
|
||||
SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// ListMemoAttachments lists attachments for a memo.
|
||||
ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error)
|
||||
// SetMemoRelations sets relations for a memo.
|
||||
// SetMemoRelations replaces the full set of relations on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all relations. Idempotent.
|
||||
SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// ListMemoRelations lists relations for a memo.
|
||||
ListMemoRelations(ctx context.Context, in *ListMemoRelationsRequest, opts ...grpc.CallOption) (*ListMemoRelationsResponse, error)
|
||||
|
|
@ -70,7 +76,8 @@ type MemoServiceClient interface {
|
|||
ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error)
|
||||
// ListMemoReactions lists reactions for a memo.
|
||||
ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error)
|
||||
// UpsertMemoReaction upserts a reaction for a memo.
|
||||
// UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
// memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
UpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
|
|
@ -301,7 +308,9 @@ func (c *memoServiceClient) BatchGetLinkMetadata(ctx context.Context, in *BatchG
|
|||
// All implementations must embed UnimplementedMemoServiceServer
|
||||
// for forward compatibility.
|
||||
type MemoServiceServer interface {
|
||||
// CreateMemo creates a memo.
|
||||
// CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
// (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
// The memo is owned by the authenticated user; requires authentication.
|
||||
CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error)
|
||||
// ListMemos lists memos with pagination and filter.
|
||||
ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error)
|
||||
|
|
@ -311,11 +320,15 @@ type MemoServiceServer interface {
|
|||
UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error)
|
||||
// DeleteMemo deletes a memo.
|
||||
DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error)
|
||||
// SetMemoAttachments sets attachments for a memo.
|
||||
// SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all attachments. Idempotent.
|
||||
SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error)
|
||||
// ListMemoAttachments lists attachments for a memo.
|
||||
ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error)
|
||||
// SetMemoRelations sets relations for a memo.
|
||||
// SetMemoRelations replaces the full set of relations on a memo with the
|
||||
// provided list (not an append). Pass the complete desired set; an empty list
|
||||
// clears all relations. Idempotent.
|
||||
SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error)
|
||||
// ListMemoRelations lists relations for a memo.
|
||||
ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error)
|
||||
|
|
@ -325,7 +338,8 @@ type MemoServiceServer interface {
|
|||
ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error)
|
||||
// ListMemoReactions lists reactions for a memo.
|
||||
ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error)
|
||||
// UpsertMemoReaction upserts a reaction for a memo.
|
||||
// UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
// memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ type Shortcut struct {
|
|||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// The title of the shortcut.
|
||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||
// The filter expression for the shortcut.
|
||||
// The CEL filter expression for the shortcut, using the same grammar as the
|
||||
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ const (
|
|||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ShortcutServiceClient interface {
|
||||
// ListShortcuts returns a list of shortcuts for a user.
|
||||
// ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
// reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
// to the ListMemos `filter` argument to reuse a saved view.
|
||||
ListShortcuts(ctx context.Context, in *ListShortcutsRequest, opts ...grpc.CallOption) (*ListShortcutsResponse, error)
|
||||
// GetShortcut gets a shortcut by name.
|
||||
GetShortcut(ctx context.Context, in *GetShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error)
|
||||
|
|
@ -105,7 +107,9 @@ func (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, in *DeleteSh
|
|||
// All implementations must embed UnimplementedShortcutServiceServer
|
||||
// for forward compatibility.
|
||||
type ShortcutServiceServer interface {
|
||||
// ListShortcuts returns a list of shortcuts for a user.
|
||||
// ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
// reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
// to the ListMemos `filter` argument to reuse a saved view.
|
||||
ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error)
|
||||
// GetShortcut gets a shortcut by name.
|
||||
GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error)
|
||||
|
|
|
|||
|
|
@ -648,15 +648,27 @@ paths:
|
|||
Default to "create_time desc".
|
||||
Supports comma-separated list of fields following AIP-132.
|
||||
Example: "pinned desc, create_time desc" or "update_time asc"
|
||||
Supported fields: pinned, create_time, update_time, name
|
||||
Supported fields: pinned, create_time, update_time, name.
|
||||
Note: order_by uses create_time / update_time, while the filter
|
||||
expression uses created_ts / updated_ts for the same timestamps.
|
||||
schema:
|
||||
type: string
|
||||
- name: filter
|
||||
in: query
|
||||
description: |-
|
||||
Optional. Filter to apply to the list results.
|
||||
Filter is a CEL expression to filter memos.
|
||||
Refer to `Shortcut.filter`.
|
||||
Optional. A CEL expression to filter memos. Combine terms with && and ||.
|
||||
Available fields:
|
||||
content (string), creator (string, e.g. "users/1"),
|
||||
created_ts / updated_ts (timestamp), pinned (bool),
|
||||
visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||
tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||
has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
||||
Note: the time fields here are created_ts / updated_ts, which differ from
|
||||
the create_time / update_time names used by order_by.
|
||||
Examples:
|
||||
pinned == true && visibility == "PUBLIC"
|
||||
tags.exists(t, t == "urgent")
|
||||
content.contains("roadmap") && created_ts > now - duration("168h")
|
||||
schema:
|
||||
type: string
|
||||
- name: showDeleted
|
||||
|
|
@ -680,7 +692,10 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- MemoService
|
||||
description: CreateMemo creates a memo.
|
||||
description: |-
|
||||
CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
(Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
The memo is owned by the authenticated user; requires authentication.
|
||||
operationId: MemoService_CreateMemo
|
||||
parameters:
|
||||
- name: memoId
|
||||
|
|
@ -889,7 +904,10 @@ paths:
|
|||
patch:
|
||||
tags:
|
||||
- MemoService
|
||||
description: SetMemoAttachments sets attachments for a memo.
|
||||
description: |-
|
||||
SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
provided list (not an append). Pass the complete desired set; an empty list
|
||||
clears all attachments. Idempotent.
|
||||
operationId: MemoService_SetMemoAttachments
|
||||
parameters:
|
||||
- name: memo
|
||||
|
|
@ -1032,7 +1050,9 @@ paths:
|
|||
post:
|
||||
tags:
|
||||
- MemoService
|
||||
description: UpsertMemoReaction upserts a reaction for a memo.
|
||||
description: |-
|
||||
UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
operationId: MemoService_UpsertMemoReaction
|
||||
parameters:
|
||||
- name: memo
|
||||
|
|
@ -1129,7 +1149,10 @@ paths:
|
|||
patch:
|
||||
tags:
|
||||
- MemoService
|
||||
description: SetMemoRelations sets relations for a memo.
|
||||
description: |-
|
||||
SetMemoRelations replaces the full set of relations on a memo with the
|
||||
provided list (not an append). Pass the complete desired set; an empty list
|
||||
clears all relations. Idempotent.
|
||||
operationId: MemoService_SetMemoRelations
|
||||
parameters:
|
||||
- name: memo
|
||||
|
|
@ -1919,7 +1942,10 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- ShortcutService
|
||||
description: ListShortcuts returns a list of shortcuts for a user.
|
||||
description: |-
|
||||
ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
to the ListMemos `filter` argument to reuse a saved view.
|
||||
operationId: ShortcutService_ListShortcuts
|
||||
parameters:
|
||||
- name: user
|
||||
|
|
@ -3262,7 +3288,10 @@ components:
|
|||
- PROTECTED
|
||||
- PUBLIC
|
||||
type: string
|
||||
description: The visibility of the memo.
|
||||
description: |-
|
||||
The visibility of the memo.
|
||||
One of PRIVATE (creator only), PROTECTED (signed-in users), or
|
||||
PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified.
|
||||
format: enum
|
||||
tags:
|
||||
readOnly: true
|
||||
|
|
@ -3569,7 +3598,9 @@ components:
|
|||
description: The title of the shortcut.
|
||||
filter:
|
||||
type: string
|
||||
description: The filter expression for the shortcut.
|
||||
description: |-
|
||||
The CEL filter expression for the shortcut, using the same grammar as the
|
||||
ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||
SignInRequest:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -131,8 +131,11 @@ a personal access token as a bearer credential. Example client config:
|
|||
|
||||
## Tool surface
|
||||
|
||||
The first version exposes a curated allowlist (`curatedOperationIDs` in
|
||||
`catalog.go`), all memo- and attachment-focused:
|
||||
The server exposes a curated allowlist (`curatedOperationIDs` in `catalog.go`),
|
||||
centered on memos and attachments, plus two read-only orientation tools:
|
||||
`shortcut_list_shortcuts` (surfaces a user's saved CEL filters for reuse with
|
||||
`memo_list_memos`) and `auth_get_current_user` (a "whoami" so an agent can
|
||||
resolve its own user — the single allowed auth/identity operation):
|
||||
|
||||
| OpenAPI operation | MCP tool |
|
||||
| --- | --- |
|
||||
|
|
@ -153,12 +156,14 @@ The first version exposes a curated allowlist (`curatedOperationIDs` in
|
|||
| `AttachmentService_ListAttachments` | `attachment_list_attachments` |
|
||||
| `AttachmentService_GetAttachment` | `attachment_get_attachment` |
|
||||
| `AttachmentService_DeleteAttachment` | `attachment_delete_attachment` |
|
||||
| `ShortcutService_ListShortcuts` | `shortcut_list_shortcuts` |
|
||||
| `AuthService_GetCurrentUser` | `auth_get_current_user` |
|
||||
|
||||
**Naming rule** (`toolNameFromOperationID`): drop the `Service` suffix from the
|
||||
subject and convert both subject and method from camelCase to snake_case, joined
|
||||
by `_`. So `MemoService_ListMemos → memo_list_memos`.
|
||||
|
||||
**Annotations** (`annotationsForMethod`) are derived from the HTTP method:
|
||||
**Annotations** (`annotationsForOperation`) start from the HTTP method:
|
||||
|
||||
| Method | ReadOnly | Destructive | Idempotent |
|
||||
| --- | --- | --- | --- |
|
||||
|
|
@ -166,6 +171,11 @@ by `_`. So `MemoService_ListMemos → memo_list_memos`.
|
|||
| DELETE | false | true | true |
|
||||
| other (POST, PATCH, …) | false | false | false |
|
||||
|
||||
A per-operation override (`idempotentOperationIDs`) then corrects cases the
|
||||
method heuristic gets wrong: `MemoService_SetMemoAttachments` and
|
||||
`MemoService_SetMemoRelations` are PATCH but declaratively replace the full set
|
||||
on a memo, so they report `IdempotentHint: true`.
|
||||
|
||||
`OpenWorldHint` is `false` for all tools. Annotations are client hints; they do
|
||||
not replace API authorization.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ var curatedOperationIDs = []string{
|
|||
"AttachmentService_ListAttachments",
|
||||
"AttachmentService_GetAttachment",
|
||||
"AttachmentService_DeleteAttachment",
|
||||
"ShortcutService_ListShortcuts",
|
||||
// The only allowed auth/identity operation: a read-only "whoami" so agents
|
||||
// can resolve the current user (e.g. for ShortcutService_ListShortcuts).
|
||||
"AuthService_GetCurrentUser",
|
||||
}
|
||||
|
||||
type registeredOperation struct {
|
||||
|
|
@ -74,7 +78,7 @@ func buildToolFromOperation(operation *openAPIOperation) (*sdkmcp.Tool, *registe
|
|||
Description: operation.Description,
|
||||
InputSchema: inputSchema,
|
||||
OutputSchema: outputSchemaForOperation(operation),
|
||||
Annotations: annotationsForMethod(operation.Method, title),
|
||||
Annotations: annotationsForOperation(operation, title),
|
||||
}
|
||||
|
||||
return tool, ®isteredOperation{
|
||||
|
|
@ -182,6 +186,26 @@ func extractSchemaDefs(schema jsonSchema) map[string]any {
|
|||
return defs
|
||||
}
|
||||
|
||||
// idempotentOperationIDs lists operations whose idempotency the HTTP-method
|
||||
// heuristic gets wrong. The "Set*" operations declaratively replace the full
|
||||
// set on a memo, so repeating an identical call converges to the same state —
|
||||
// idempotent — even though they are served over PATCH (which the heuristic
|
||||
// treats as non-idempotent).
|
||||
var idempotentOperationIDs = map[string]bool{
|
||||
"MemoService_SetMemoAttachments": true,
|
||||
"MemoService_SetMemoRelations": true,
|
||||
}
|
||||
|
||||
// annotationsForOperation derives the method-based annotations and then applies
|
||||
// per-operation overrides that the HTTP method alone cannot express.
|
||||
func annotationsForOperation(operation *openAPIOperation, title string) *sdkmcp.ToolAnnotations {
|
||||
annotations := annotationsForMethod(operation.Method, title)
|
||||
if idempotentOperationIDs[operation.OperationID] {
|
||||
annotations.IdempotentHint = true
|
||||
}
|
||||
return annotations
|
||||
}
|
||||
|
||||
func annotationsForMethod(method string, title string) *sdkmcp.ToolAnnotations {
|
||||
openWorld := false
|
||||
destructive := false
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ import (
|
|||
)
|
||||
|
||||
func TestCuratedOperationIDsStayMemoFocused(t *testing.T) {
|
||||
require.Len(t, curatedOperationIDs, 17)
|
||||
require.Len(t, curatedOperationIDs, 19)
|
||||
|
||||
for _, operationID := range curatedOperationIDs {
|
||||
require.NotContains(t, operationID, "Admin")
|
||||
require.NotContains(t, operationID, "AuthService_")
|
||||
// AuthService_GetCurrentUser is the single allowed auth op (read-only
|
||||
// "whoami"); the rest of the auth/identity surface stays off MCP.
|
||||
if operationID != "AuthService_GetCurrentUser" {
|
||||
require.NotContains(t, operationID, "AuthService_")
|
||||
}
|
||||
require.NotContains(t, operationID, "UserService_")
|
||||
require.NotContains(t, operationID, "AIService_")
|
||||
require.NotContains(t, operationID, "IdentityProviderService_")
|
||||
|
|
@ -100,6 +104,53 @@ func TestBuildToolFromOperationIncludesRequestBodySchema(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBuildToolFromOperationExposesCurrentUser(t *testing.T) {
|
||||
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||
require.NoError(t, err)
|
||||
registry, err := buildOperationRegistry(spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
tool, operation := buildToolFromOperation(registry["AuthService_GetCurrentUser"])
|
||||
require.Equal(t, "auth_get_current_user", tool.Name)
|
||||
require.Equal(t, "GET", operation.Method)
|
||||
require.True(t, tool.Annotations.ReadOnlyHint)
|
||||
}
|
||||
|
||||
func TestBuildToolFromOperationExposesListShortcuts(t *testing.T) {
|
||||
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||
require.NoError(t, err)
|
||||
registry, err := buildOperationRegistry(spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
tool, operation := buildToolFromOperation(registry["ShortcutService_ListShortcuts"])
|
||||
require.Equal(t, "shortcut_list_shortcuts", tool.Name)
|
||||
require.Equal(t, "GET", operation.Method)
|
||||
require.True(t, tool.Annotations.ReadOnlyHint)
|
||||
|
||||
input, ok := tool.InputSchema.(jsonSchema)
|
||||
require.True(t, ok)
|
||||
properties, ok := input["properties"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, properties, "user")
|
||||
}
|
||||
|
||||
func TestBuildToolFromOperationMarksSetOperationsIdempotent(t *testing.T) {
|
||||
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||
require.NoError(t, err)
|
||||
registry, err := buildOperationRegistry(spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, operationID := range []string{"MemoService_SetMemoAttachments", "MemoService_SetMemoRelations"} {
|
||||
tool, operation := buildToolFromOperation(registry[operationID])
|
||||
require.Equal(t, "PATCH", operation.Method, operationID)
|
||||
// PATCH is non-idempotent by the method heuristic, but the per-operation
|
||||
// override restores the declarative "set" semantics.
|
||||
require.True(t, tool.Annotations.IdempotentHint, operationID)
|
||||
require.False(t, tool.Annotations.ReadOnlyHint, operationID)
|
||||
require.False(t, *tool.Annotations.DestructiveHint, operationID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCuratedToolsHasUniqueNames(t *testing.T) {
|
||||
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
67
server/router/mcp/evals/README.md
Normal file
67
server/router/mcp/evals/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# MCP Evaluations
|
||||
|
||||
Task-level evaluations for the memos MCP server. Where the `*_test.go` files
|
||||
verify the server *plumbing* (schema resolution, tool naming, annotations),
|
||||
these check the thing that actually matters for an MCP server: **can an LLM
|
||||
accomplish realistic tasks by composing the tools?** They are the regression
|
||||
net for tool descriptions and discoverability — e.g. a bad `filter` description
|
||||
leaves the unit tests green but makes question 3/5/9 unanswerable.
|
||||
|
||||
`memos_eval.xml` holds 10 question/answer pairs in the format used by the
|
||||
mcp-builder skill. Each question is independent, read-only, requires multiple
|
||||
tool calls, and has a single string-comparable answer.
|
||||
|
||||
## Why a fresh seeded instance (not the public demo)
|
||||
|
||||
Answers are pinned to the deterministic seed in
|
||||
[`store/seed/sqlite/01__dump.sql`](../../../../store/seed/sqlite/01__dump.sql)
|
||||
(10 memos — 7 top-level + 3 comments — 2 users, 12 reactions, no attachments,
|
||||
no shortcuts).
|
||||
|
||||
The public demo (`demo.usememos.com`) signs everyone into the **same shared
|
||||
`demo` account**, so visitors continually add/edit/delete memos and reactions.
|
||||
Its data has already diverged from the seed — do **not** evaluate against it.
|
||||
|
||||
The seed uses **relative** timestamps (`strftime('now','-N days')`), so the
|
||||
questions avoid absolute dates and rely only on relative ordering, counts, and
|
||||
content, all of which are stable across re-seeds.
|
||||
|
||||
## Running an evaluation
|
||||
|
||||
1. Launch a throwaway demo-mode instance (SQLite, auto-seeded) on a free port:
|
||||
|
||||
```bash
|
||||
go run ./cmd/memos --demo --driver sqlite \
|
||||
--port 8099 --data "$(mktemp -d)" \
|
||||
--instance-url http://localhost:8099
|
||||
```
|
||||
|
||||
2. The MCP endpoint is `http://localhost:8099/mcp`. Authenticate with the seed's
|
||||
demo personal access token:
|
||||
|
||||
```
|
||||
Authorization: Bearer memos_pat_demo
|
||||
```
|
||||
|
||||
3. Point an MCP client / eval harness at that endpoint and have the model answer
|
||||
each `<question>`, then string-compare against each `<answer>`.
|
||||
|
||||
Quick manual check of a single tool call:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8099/mcp \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Authorization: Bearer memos_pat_demo' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"memo_list_memos","arguments":{"filter":"pinned == true"}}}'
|
||||
```
|
||||
|
||||
## Notes for whoever extends this
|
||||
|
||||
- The CEL `tag` field does **not** support `==`; filter tags with
|
||||
`"work" in tags` (or `tags.exists(t, t == "work")`), not `tag == "work"`.
|
||||
- `memo_list_memos` returns only top-level memos; comments are reached via
|
||||
`memo_list_memo_comments`.
|
||||
- The seed defines no shortcuts and no attachments, so `shortcut_list_shortcuts`
|
||||
and the attachment tools return empty sets against a fresh seed. Add seed rows
|
||||
before writing questions that depend on them.
|
||||
62
server/router/mcp/evals/memos_eval.xml
Normal file
62
server/router/mcp/evals/memos_eval.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Task-level evaluation for the memos MCP server.
|
||||
|
||||
These questions test whether an LLM can accomplish realistic tasks by
|
||||
composing the curated MCP tools (memo_list_memos with CEL filters,
|
||||
memo_list_memo_comments, memo_list_memo_reactions, shortcut_list_shortcuts,
|
||||
auth_get_current_user, ...). They are NOT unit tests of the server plumbing —
|
||||
see *_test.go for that.
|
||||
|
||||
Answers are pinned to the deterministic seed in
|
||||
store/seed/sqlite/01__dump.sql and were verified by querying a fresh
|
||||
demo-mode instance. Run against a freshly seeded instance ONLY (the public
|
||||
demo at demo.usememos.com is a shared, mutable account and will not match).
|
||||
See README.md in this directory for how to launch one.
|
||||
|
||||
Questions deliberately avoid absolute dates: the seed uses relative
|
||||
timestamps (strftime('now','-N days')), so only relative ordering, counts,
|
||||
and content are stable.
|
||||
-->
|
||||
<evaluation>
|
||||
<qa_pair>
|
||||
<question>Look at every memo in the instance and add up all the emoji reactions across them. What is the total number of reactions?</question>
|
||||
<answer>12</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>How many memos are pinned?</question>
|
||||
<answer>2</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Considering only the memos that are NOT pinned, one of them has more reactions than any other. That memo is about a book the author started reading. What is the title of that book?</question>
|
||||
<answer>Deep Work</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Two memos are pinned. One contains external hyperlinks and the other does not. Give the single tag of the pinned memo that contains hyperlinks.</question>
|
||||
<answer>sponsors</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>How many memos contain at least one fenced code block?</question>
|
||||
<answer>2</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>How many distinct users have authored the memos in this instance?</question>
|
||||
<answer>2</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Find the memo that is a cheat sheet of git commands. How many comments does it have?</question>
|
||||
<answer>2</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Find the memo containing a movie watchlist. What is the username of the user who created it?</question>
|
||||
<answer>alice</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Exactly one memo is a travel bucket list. It names a Nordic country the author plans to visit in winter to see the Northern Lights. Which country?</question>
|
||||
<answer>Iceland</answer>
|
||||
</qa_pair>
|
||||
<qa_pair>
|
||||
<question>Using the authenticated session, what is the username (login name) of the account these memos belong to?</question>
|
||||
<answer>demo</answer>
|
||||
</qa_pair>
|
||||
</evaluation>
|
||||
|
|
@ -125,6 +125,8 @@ export type Memo = Message<"memos.api.v1.Memo"> & {
|
|||
|
||||
/**
|
||||
* The visibility of the memo.
|
||||
* One of PRIVATE (creator only), PROTECTED (signed-in users), or
|
||||
* PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified.
|
||||
*
|
||||
* @generated from field: memos.api.v1.Visibility visibility = 9;
|
||||
*/
|
||||
|
|
@ -338,16 +340,28 @@ export type ListMemosRequest = Message<"memos.api.v1.ListMemosRequest"> & {
|
|||
* Default to "create_time desc".
|
||||
* Supports comma-separated list of fields following AIP-132.
|
||||
* Example: "pinned desc, create_time desc" or "update_time asc"
|
||||
* Supported fields: pinned, create_time, update_time, name
|
||||
* Supported fields: pinned, create_time, update_time, name.
|
||||
* Note: order_by uses create_time / update_time, while the filter
|
||||
* expression uses created_ts / updated_ts for the same timestamps.
|
||||
*
|
||||
* @generated from field: string order_by = 4;
|
||||
*/
|
||||
orderBy: string;
|
||||
|
||||
/**
|
||||
* Optional. Filter to apply to the list results.
|
||||
* Filter is a CEL expression to filter memos.
|
||||
* Refer to `Shortcut.filter`.
|
||||
* Optional. A CEL expression to filter memos. Combine terms with && and ||.
|
||||
* Available fields:
|
||||
* content (string), creator (string, e.g. "users/1"),
|
||||
* created_ts / updated_ts (timestamp), pinned (bool),
|
||||
* visibility (string: PRIVATE | PROTECTED | PUBLIC),
|
||||
* tags (list<string>; match with `"work" in tags`, not `tag == "work"`),
|
||||
* has_task_list / has_link / has_code / has_incomplete_tasks (bool).
|
||||
* Note: the time fields here are created_ts / updated_ts, which differ from
|
||||
* the create_time / update_time names used by order_by.
|
||||
* Examples:
|
||||
* pinned == true && visibility == "PUBLIC"
|
||||
* tags.exists(t, t == "urgent")
|
||||
* content.contains("roadmap") && created_ts > now - duration("168h")
|
||||
*
|
||||
* @generated from field: string filter = 5;
|
||||
*/
|
||||
|
|
@ -1193,6 +1207,8 @@ export const LinkMetadataSchema: GenMessage<LinkMetadata> = /*@__PURE__*/
|
|||
messageDesc(file_api_v1_memo_service, 32);
|
||||
|
||||
/**
|
||||
* Visibility controls who can read a memo.
|
||||
*
|
||||
* @generated from enum memos.api.v1.Visibility
|
||||
*/
|
||||
export enum Visibility {
|
||||
|
|
@ -1202,16 +1218,22 @@ export enum Visibility {
|
|||
VISIBILITY_UNSPECIFIED = 0,
|
||||
|
||||
/**
|
||||
* PRIVATE: only the creator can read the memo.
|
||||
*
|
||||
* @generated from enum value: PRIVATE = 1;
|
||||
*/
|
||||
PRIVATE = 1,
|
||||
|
||||
/**
|
||||
* PROTECTED: signed-in users of the instance can read the memo.
|
||||
*
|
||||
* @generated from enum value: PROTECTED = 2;
|
||||
*/
|
||||
PROTECTED = 2,
|
||||
|
||||
/**
|
||||
* PUBLIC: anyone, including anonymous visitors, can read the memo.
|
||||
*
|
||||
* @generated from enum value: PUBLIC = 3;
|
||||
*/
|
||||
PUBLIC = 3,
|
||||
|
|
@ -1228,7 +1250,9 @@ export const VisibilitySchema: GenEnum<Visibility> = /*@__PURE__*/
|
|||
*/
|
||||
export const MemoService: GenService<{
|
||||
/**
|
||||
* CreateMemo creates a memo.
|
||||
* CreateMemo creates a memo. The request body is a Memo; set its content
|
||||
* (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE).
|
||||
* The memo is owned by the authenticated user; requires authentication.
|
||||
*
|
||||
* @generated from rpc memos.api.v1.MemoService.CreateMemo
|
||||
*/
|
||||
|
|
@ -1278,7 +1302,9 @@ export const MemoService: GenService<{
|
|||
output: typeof EmptySchema;
|
||||
},
|
||||
/**
|
||||
* SetMemoAttachments sets attachments for a memo.
|
||||
* SetMemoAttachments replaces the full set of attachments on a memo with the
|
||||
* provided list (not an append). Pass the complete desired set; an empty list
|
||||
* clears all attachments. Idempotent.
|
||||
*
|
||||
* @generated from rpc memos.api.v1.MemoService.SetMemoAttachments
|
||||
*/
|
||||
|
|
@ -1298,7 +1324,9 @@ export const MemoService: GenService<{
|
|||
output: typeof ListMemoAttachmentsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* SetMemoRelations sets relations for a memo.
|
||||
* SetMemoRelations replaces the full set of relations on a memo with the
|
||||
* provided list (not an append). Pass the complete desired set; an empty list
|
||||
* clears all relations. Idempotent.
|
||||
*
|
||||
* @generated from rpc memos.api.v1.MemoService.SetMemoRelations
|
||||
*/
|
||||
|
|
@ -1348,7 +1376,8 @@ export const MemoService: GenService<{
|
|||
output: typeof ListMemoReactionsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* UpsertMemoReaction upserts a reaction for a memo.
|
||||
* UpsertMemoReaction adds or updates the authenticated user's reaction on a
|
||||
* memo. The reaction's content_id is the memo's resource name (memos/{memo}).
|
||||
*
|
||||
* @generated from rpc memos.api.v1.MemoService.UpsertMemoReaction
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ export type Shortcut = Message<"memos.api.v1.Shortcut"> & {
|
|||
title: string;
|
||||
|
||||
/**
|
||||
* The filter expression for the shortcut.
|
||||
* The CEL filter expression for the shortcut, using the same grammar as the
|
||||
* ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||
*
|
||||
* @generated from field: string filter = 3;
|
||||
*/
|
||||
|
|
@ -196,7 +197,9 @@ export const DeleteShortcutRequestSchema: GenMessage<DeleteShortcutRequest> = /*
|
|||
*/
|
||||
export const ShortcutService: GenService<{
|
||||
/**
|
||||
* ListShortcuts returns a list of shortcuts for a user.
|
||||
* ListShortcuts returns a user's saved shortcuts. Each shortcut is a named,
|
||||
* reusable CEL filter (see Shortcut.filter); pass its filter string directly
|
||||
* to the ListMemos `filter` argument to reuse a saved view.
|
||||
*
|
||||
* @generated from rpc memos.api.v1.ShortcutService.ListShortcuts
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue