Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
themeEmacs
[McpServerToolType]
public static class NoteTool
{
    [McpServerTool, Description("웹노리 노트에 노트를 추가합니다.")]
    public static async Task<string> AddNote(ActorService actorService, 
        [Description("노트의 제목으로 필수값입니다.")] string title,
        [Description("노트의 컨텐츠값으로 필수값입니다.")] string content,
        [Description("노트의 카테고리입니다.")] string? category, 
        [Description("노트에 위치정보가 있다면 latitude를 입력")] double? latitude, 
        [Description("노트에 위치정보가 있다면 longitude 입력")] double? longitude,
        [Description("노트에 임베딩된 값이 있다면 입력")] float[]? tagsEmbeddedAsSingle
        )
    {
        var note = new NoteDocument
        {
            Title = title,
            Category = category,
            Content = content,
            Latitude = latitude,
            Longitude = longitude,
            CreatedAt = DateTime.UtcNow,
            TagsEmbeddedAsSingle = new RavenVector<float>(tagsEmbeddedAsSingle)
        };
        
        actorService.RecordActor.Tell(new AddNoteCommand()
        {
            Title = title,
            Category = category,
            Content = content,
            Latitude = latitude,
            Longitude = longitude,
            TagsEmbeddedAsSingle = new RavenVector<float>(tagsEmbeddedAsSingle)
        }, ActorRefs.NoSender);
        
        return JsonSerializer.Serialize(note);
    }
    
    [McpServerTool, Description("웹노리 노트에서 Text검색을 합니다. 최소 하나값이 필수입니다.")]
    public static async Task<string> SearchNoteByText(ActorService actorService, 
        [Description("웹노리 노트에서 타이틀을 키워드 검색합니다.")] string? title, 
        [Description("웹노리 노트에서 컨텐츠를 키워드 검색합니다.")] string? content, 
        [Description("웹노리 노트에서 카테고리를 키워드 검색합니다.")] string? category)
    {
        var result = await actorService.SearchActor.Ask(new SearchNoteByTextCommand()
        {
            Title = title,
            Content = content,
            Category = category
        }, TimeSpan.FromSeconds(5));
        
        if (result is SearchNoteActorResult searchResult)
        {
            return JsonSerializer.Serialize(searchResult.Notes);
        }
        
        return "Failed to get note history.";
    }
    
    [McpServerTool, Description("웹노리 노트에서 반경검색을 합니다. 모두 필수값입니다.")]
    public static async Task<string> SearchNoteByRadius(ActorService actorService, 
        [Description("This is the latitude of the note ")] double latitude, 
        [Description("This is the longitude of the note ")] double longitude, 
        [Description("This is the radius(m) of the note ")] double radius)
    {
        var result = await actorService.SearchActor.Ask(new SearchNoteByRadiusActorCommand()
        {
            Latitude = latitude,
            Longitude = longitude,
            Radius = radius
        }, TimeSpan.FromSeconds(5));
        
        if (result is SearchNoteActorResult searchResult)
        {
            return JsonSerializer.Serialize(searchResult.Notes);
        }
        
        return "Failed to get note history.";
    }
    
    [McpServerTool, Description("웹노리 노트에서 벡터검색을 합니다. 모두 필수값입니다.")]
    public static async Task<string> SearchNoteByVector(ActorService actorService, 
        [Description("This is the vector of the note  If there is no value, enter null.")] float[] vector, 
        [Description("This is the top N of the note  If there is no value, enter null.")] int topN)
    {
        var result = await actorService.SearchActor.Ask(new SearchNoteByVectorCommand()
        {
            Vector = vector,
            TopN = topN
        }, TimeSpan.FromSeconds(5));
        
        if (result is SearchNoteActorResult searchResult)
        {
            return JsonSerializer.Serialize(searchResult.Notes);
        }
        
        return "Failed to get note history.";
    }
    
    
    [McpServerTool, Description("웹노리 노트에서 최근 추가된 노트를 가져옵니다.")]
    public static async Task<string> GetNoteHistory(ActorService actorService)
    {
        //SearchNoteActorResult
        var result = await actorService.HistoryActor.Ask(new GetNoteHistoryCommand(), TimeSpan.FromSeconds(5));
        
        if (result is SearchNoteActorResult searchResult)
        {
            return JsonSerializer.Serialize(searchResult.Notes);
        }
        
        return "Failed to get note history.";
    }
    
    [McpServerTool, Description("웹노리 노트에서 최근 검색된 노트를 가져옵니다.")]
    public static async Task<string> GetNoteSearchHistory(ActorService actorService)
    {
        //SearchNoteActorResult
        var result = await actorService.HistoryActor.Ask(new GetNoteSearchHistoryCommand(), TimeSpan.FromSeconds(5));
        
        if (result is SearchNoteActorResult searchResult)
        {
            return JsonSerializer.Serialize(searchResult.Notes);
        }
        
        return "Failed to get note history.";
    }

    
}
  • Agent LLM이 툴을 선택할때 필요로하는 Description과 구현체를 정리하며 MCP Server에서 사실상 핵심코드영역입니다.
  • 작동코드는 모두 액터객체를 이용한 이벤트 방식을 사용합니다.
    • Tell : 응답값을 바로 알필요가 없을이 이벤트 전송시사용
    • Ask : 이벤트 전송후 해당 이벤트에 반응하는 응답값을 대기할때 사용
  • Local Actor를 이용할수도 있지만 복제구성을 해 Client-Server 작동으로 구분하였으며 여기서는 별도로 구동된 Remote에 위치한 서버의 액터를 이용합니다. 
  • 초기개발은 단일Local StandAlone으로만 작동가능하며 이후 전략에따라 실제 작동액터는 별도 구성된 서버로 코드변경없이 분리할수 있는것이 액터모델의 장점입니다.


클라이언트 서버모델

Code Block
themeEmacs
public class ActorService
{
    private readonly ActorSystem actorSystem;
    
    public IActorRef SearchActor { get; set; }
    
    public IActorRef RecordActor { get; set; }
    
    public IActorRef HistoryActor { get; set; }
    
    public ActorService(bool serverMode)
    {
        Console.WriteLine($"ActorService initialized in {(serverMode ? "Server" : "Client")} mode.");

        // HOCON 설정
        var config = ConfigurationFactory.ParseString($@"
            akka {{
                actor {{
                    provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
                }}
                remote {{
                    dot-netty.tcp {{
                        hostname = ""127.0.0.1""
                        port = {(serverMode ? 5500 : 0)} // 서버 모드일 때만 포트 5500 사용
                    }}
                }}
            }}
        ");

        actorSystem = ActorSystem.Create("MyActorSystem", config);

        if (serverMode)
        {
            // 서버 모드일 때만 작동액터 생성 : MCP Server 
            SearchActor = actorSystem.ActorOf<SearchActor>("search-actor");
            RecordActor = actorSystem.ActorOf<RecordActor>("record-actor");
            HistoryActor = actorSystem.ActorOf<HistoryActor>("history-actor");

            RecordActor.Tell(new SetHistoryActorCommand()
            {
                HistoryActor = HistoryActor
            });

            SearchActor.Tell(new SetHistoryActorCommand()
            {
                HistoryActor = HistoryActor
            });
        }
        else
        {
            // 클라이언트 모드일 때 원격 액터 참조 : MCP Client
            var remoteAddress = "akka.tcp://MyActorSystem@127.0.0.1:5500";
            SearchActor = actorSystem.ActorSelection($"{remoteAddress}/user/search-actor")
                .ResolveOne(TimeSpan.FromSeconds(3)).Result;

            RecordActor = actorSystem.ActorSelection($"{remoteAddress}/user/record-actor")
                .ResolveOne(TimeSpan.FromSeconds(3)).Result;

            HistoryActor = actorSystem.ActorSelection($"{remoteAddress}/user/history-actor")
                .ResolveOne(TimeSpan.FromSeconds(3)).Result;
        }
    }
    
}

...